--- /dev/null
+ <?xml version="1.0" encoding="UTF-8"?>
+ <classpath>
+ <classpathentry kind="src" path="src"/>
+ <classpathentry exported="true" kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+ <classpathentry kind="output" path="bin"/>
+ </classpath>
--- /dev/null
+ *.class
+ bin/**
+ build/**
+
+ # Mobile Tools for Java (J2ME)
+ .mtj.tmp/
+
+ # Package Files #
+ *.jar
+ *.war
+ *.ear
+
+ # Generated docs
+ docs/**
+
+ # Maven artifacts
+ target/**
+
+ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+ hs_err_pid*
+
+ # Editor backup files
+ *.java~
+ *.xml~
+
+ # Scratch space
+ misc/**
+ /.project~
+
+ pmd.bash
+ pmd-results.html
+ examples/*.sh
+
+ # Fonts for testing
+ fonts/**
--- /dev/null
+ <?xml version="1.0" encoding="UTF-8"?>
+ <projectDescription>
+ <name>jexer</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>org.eclipse.jdt.core.javabuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ </natures>
+ </projectDescription>
--- /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;
+
+ /**
+ * Scrollable provides a public API for horizontal and vertical scrollbars.
+ * Note that not all Scrollables support both horizontal and vertical
+ * scrolling; for those that only support a subset, it is expected that the
+ * methods corresponding to the missing scrollbar quietly succeed without
+ * throwing any exceptions.
+ */
+ public interface Scrollable {
+
+ /**
+ * Get the horizontal scrollbar, or null if this Viewport does not
+ * support horizontal scrolling.
+ *
+ * @return the horizontal scrollbar
+ */
+ public THScroller getHorizontalScroller();
+
+ /**
+ * Get the vertical scrollbar, or null if this Viewport does not support
+ * vertical scrolling.
+ *
+ * @return the vertical scrollbar
+ */
+ public TVScroller getVerticalScroller();
+
+ /**
+ * Get the value that corresponds to being on the top edge of the
+ * vertical scroll bar.
+ *
+ * @return the scroll value
+ */
+ public int getTopValue();
+
+ /**
+ * Set the value that corresponds to being on the top edge of the
+ * vertical scroll bar.
+ *
+ * @param topValue the new scroll value
+ */
+ public void setTopValue(final int topValue);
+
+ /**
+ * Get the value that corresponds to being on the bottom edge of the
+ * vertical scroll bar.
+ *
+ * @return the scroll value
+ */
+ public int getBottomValue();
+
+ /**
+ * Set the value that corresponds to being on the bottom edge of the
+ * vertical scroll bar.
+ *
+ * @param bottomValue the new scroll value
+ */
+ public void setBottomValue(final int bottomValue);
+
+ /**
+ * Get current value of the vertical scroll.
+ *
+ * @return the scroll value
+ */
+ public int getVerticalValue();
+
+ /**
+ * Set current value of the vertical scroll.
+ *
+ * @param value the new scroll value
+ */
+ public void setVerticalValue(final int value);
+
+ /**
+ * Get the increment for clicking on an arrow on the vertical scrollbar.
+ *
+ * @return the increment value
+ */
+ public int getVerticalSmallChange();
+
+ /**
+ * Set the increment for clicking on an arrow on the vertical scrollbar.
+ *
+ * @param smallChange the new increment value
+ */
+ public void setVerticalSmallChange(final int smallChange);
+
+ /**
+ * Get the increment for clicking in the bar between the box and an
+ * arrow on the vertical scrollbar.
+ *
+ * @return the increment value
+ */
+ public int getVerticalBigChange();
+
+ /**
+ * Set the increment for clicking in the bar between the box and an
+ * arrow on the vertical scrollbar.
+ *
+ * @param bigChange the new increment value
+ */
+ public void setVerticalBigChange(final int bigChange);
+
+ /**
+ * Perform a small step change up.
+ */
+ public void verticalDecrement();
+
+ /**
+ * Perform a small step change down.
+ */
+ public void verticalIncrement();
+
+ /**
+ * Perform a big step change up.
+ */
+ public void bigVerticalDecrement();
+
+ /**
+ * Perform a big step change down.
+ */
+ public void bigVerticalIncrement();
+
+ /**
+ * Go to the top edge of the vertical scroller.
+ */
+ public void toTop();
+
+ /**
+ * Go to the bottom edge of the vertical scroller.
+ */
+ public void toBottom();
+
+ /**
+ * Get the value that corresponds to being on the left edge of the
+ * horizontal scroll bar.
+ *
+ * @return the scroll value
+ */
+ public int getLeftValue();
+
+ /**
+ * Set the value that corresponds to being on the left edge of the
+ * horizontal scroll bar.
+ *
+ * @param leftValue the new scroll value
+ */
+ public void setLeftValue(final int leftValue);
+
+ /**
+ * Get the value that corresponds to being on the right edge of the
+ * horizontal scroll bar.
+ *
+ * @return the scroll value
+ */
+ public int getRightValue();
+
+ /**
+ * Set the value that corresponds to being on the right edge of the
+ * horizontal scroll bar.
+ *
+ * @param rightValue the new scroll value
+ */
+ public void setRightValue(final int rightValue);
+
+ /**
+ * Get current value of the horizontal scroll.
+ *
+ * @return the scroll value
+ */
+ public int getHorizontalValue();
+
+ /**
+ * Set current value of the horizontal scroll.
+ *
+ * @param value the new scroll value
+ */
+ public void setHorizontalValue(final int value);
+
+ /**
+ * Get the increment for clicking on an arrow on the horizontal
+ * scrollbar.
+ *
+ * @return the increment value
+ */
+ public int getHorizontalSmallChange();
+
+ /**
+ * Set the increment for clicking on an arrow on the horizontal
+ * scrollbar.
+ *
+ * @param smallChange the new increment value
+ */
+ public void setHorizontalSmallChange(final int smallChange);
+
+ /**
+ * Get the increment for clicking in the bar between the box and an
+ * arrow on the horizontal scrollbar.
+ *
+ * @return the increment value
+ */
+ public int getHorizontalBigChange();
+
+ /**
+ * Set the increment for clicking in the bar between the box and an
+ * arrow on the horizontal scrollbar.
+ *
+ * @param bigChange the new increment value
+ */
+ public void setHorizontalBigChange(final int bigChange);
+
+ /**
+ * Perform a small step change left.
+ */
+ public void horizontalDecrement();
+
+ /**
+ * Perform a small step change right.
+ */
+ public void horizontalIncrement();
+
+ /**
+ * Perform a big step change left.
+ */
+ public void bigHorizontalDecrement();
+
+ /**
+ * Perform a big step change right.
+ */
+ public void bigHorizontalIncrement();
+
+ /**
+ * Go to the left edge of the horizontal scroller.
+ */
+ public void toLeft();
+
+ /**
+ * Go to the right edge of the horizontal scroller.
+ */
+ public void toRight();
+
+ /**
+ * Go to the top-left edge of the horizontal and vertical scrollers.
+ */
+ public void toHome();
+
+ /**
+ * Go to the bottom-right edge of the horizontal and vertical scrollers.
+ */
+ public void toEnd();
+
+ }
--- /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;
+
+ /**
+ * A TAction represents a simple action to perform in response to the user.
+ *
+ * @see TButton
+ */
+ public abstract class TAction {
+
+ /**
+ * The widget that called this action's DO() method. Note that this
+ * field could be null, for example if executed as a timer action.
+ */
+ public TWidget source;
+
+ /**
+ * An optional bit of data associated with this action.
+ */
+ public Object data;
+
+ /**
+ * Call DO() with source widget set.
+ *
+ * @param source the source widget
+ */
+ public final void DO(final TWidget source) {
+ this.source = source;
+ DO();
+ }
+
+ /**
+ * Call DO() with source widget and data set.
+ *
+ * @param source the source widget
+ * @param data the data
+ */
+ public final void DO(final TWidget source, final Object data) {
+ this.source = source;
+ this.data = data;
+ DO();
+ }
+
+ /**
+ * Various classes will call DO() when they are clicked/selected.
+ */
+ public abstract void DO();
+ }
--- /dev/null
+ /*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+ package jexer;
+
+ import java.io.File;
+ import java.io.InputStream;
+ import java.io.IOException;
+ import java.io.OutputStream;
+ import java.io.PrintWriter;
+ import java.io.Reader;
+ import java.io.UnsupportedEncodingException;
+ import java.text.MessageFormat;
+ import java.util.ArrayList;
+ import java.util.Collections;
+ import java.util.Date;
+ import java.util.HashMap;
+ import java.util.LinkedList;
+ import java.util.List;
+ import java.util.Map;
+ import java.util.ResourceBundle;
+
+ import jexer.bits.Cell;
+ import jexer.bits.CellAttributes;
+ import jexer.bits.ColorTheme;
+ import jexer.bits.StringUtils;
+ import jexer.event.TCommandEvent;
+ import jexer.event.TInputEvent;
+ import jexer.event.TKeypressEvent;
+ import jexer.event.TMenuEvent;
+ import jexer.event.TMouseEvent;
+ import jexer.event.TResizeEvent;
+ import jexer.backend.Backend;
+ import jexer.backend.MultiBackend;
+ import jexer.backend.Screen;
+ import jexer.backend.SwingBackend;
+ import jexer.backend.ECMA48Backend;
+ import jexer.backend.TWindowBackend;
+ import jexer.menu.TMenu;
+ import jexer.menu.TMenuItem;
+ import jexer.menu.TSubMenu;
+ import static jexer.TCommand.*;
+ import static jexer.TKeypress.*;
+
+ /**
+ * TApplication is the main driver class for a full Text User Interface
+ * application. It manages windows, provides a menu bar and status bar, and
+ * processes events received from the user.
+ */
+ public class TApplication implements Runnable {
+
+ /**
+ * Translated strings.
+ */
+ private static final ResourceBundle i18n = ResourceBundle.getBundle(TApplication.class.getName());
+
+ // ------------------------------------------------------------------------
+ // Constants --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * If true, emit thread stuff to System.err.
+ */
+ private static final boolean debugThreads = false;
+
+ /**
+ * If true, emit events being processed to System.err.
+ */
+ private static final boolean debugEvents = false;
+
+ /**
+ * If true, do "smart placement" on new windows that are not specified to
+ * be centered.
+ */
+ private static final boolean smartWindowPlacement = true;
+
+ /**
+ * Two backend types are available.
+ */
+ public static enum BackendType {
+ /**
+ * A Swing JFrame.
+ */
+ SWING,
+
+ /**
+ * An ECMA48 / ANSI X3.64 / XTERM style terminal.
+ */
+ ECMA48,
+
+ /**
+ * Synonym for ECMA48.
+ */
+ XTERM
+ }
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The primary event handler thread.
+ */
+ private volatile WidgetEventHandler primaryEventHandler;
+
+ /**
+ * The secondary event handler thread.
+ */
+ private volatile WidgetEventHandler secondaryEventHandler;
+
+ /**
+ * The screen handler thread.
+ */
+ private volatile ScreenHandler screenHandler;
+
+ /**
+ * The widget receiving events from the secondary event handler thread.
+ */
+ private volatile TWidget secondaryEventReceiver;
+
+ /**
+ * Access to the physical screen, keyboard, and mouse.
+ */
+ private Backend backend;
+
+ /**
+ * Actual mouse coordinate X.
+ */
+ private int mouseX;
+
+ /**
+ * Actual mouse coordinate Y.
+ */
+ 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 int oldDrawnMouseX;
+
+ /**
+ * Old drawn version mouse coordinate Y.
+ */
+ private int oldDrawnMouseY;
+
+ /**
+ * Old drawn version mouse cell.
+ */
+ private Cell oldDrawnMouseCell = new Cell();
+
+ /**
+ * The last mouse up click time, used to determine if this is a mouse
+ * double-click.
+ */
+ private long lastMouseUpTime;
+
+ /**
+ * The amount of millis between mouse up events to assume a double-click.
+ */
+ private long doubleClickTime = 250;
+
+ /**
+ * Event queue that is filled by run().
+ */
+ private List<TInputEvent> fillEventQueue;
+
+ /**
+ * Event queue that will be drained by either primary or secondary
+ * Thread.
+ */
+ private List<TInputEvent> drainEventQueue;
+
+ /**
+ * Top-level menus in this application.
+ */
+ private List<TMenu> menus;
+
+ /**
+ * Stack of activated sub-menus in this application.
+ */
+ private List<TMenu> subMenus;
+
+ /**
+ * The currently active menu.
+ */
+ private TMenu activeMenu = null;
+
+ /**
+ * Active keyboard accelerators.
+ */
+ private Map<TKeypress, TMenuItem> accelerators;
+
+ /**
+ * All menu items.
+ */
+ private List<TMenuItem> menuItems;
+
+ /**
+ * Windows and widgets pull colors from this ColorTheme.
+ */
+ private ColorTheme theme;
+
+ /**
+ * The top-level windows (but not menus).
+ */
+ private List<TWindow> windows;
+
+ /**
+ * The currently acive window.
+ */
+ private TWindow activeWindow = null;
+
+ /**
+ * Timers that are being ticked.
+ */
+ private List<TTimer> timers;
+
+ /**
+ * When true, the application has been started.
+ */
+ private volatile boolean started = false;
+
+ /**
+ * When true, exit the application.
+ */
+ private volatile boolean quit = false;
+
+ /**
+ * When true, repaint the entire screen.
+ */
+ private volatile boolean repaint = true;
+
+ /**
+ * Y coordinate of the top edge of the desktop. For now this is a
+ * constant. Someday it would be nice to have a multi-line menu or
+ * toolbars.
+ */
+ private int desktopTop = 1;
+
+ /**
+ * Y coordinate of the bottom edge of the desktop.
+ */
+ private int desktopBottom;
+
+ /**
+ * An optional TDesktop background window that is drawn underneath
+ * everything else.
+ */
+ private TDesktop desktop;
+
+ /**
+ * If true, focus follows mouse: windows automatically raised if the
+ * mouse passes over them.
+ */
+ private boolean focusFollowsMouse = false;
+
+ /**
+ * If true, display a text-based mouse cursor.
+ */
+ private boolean textMouse = true;
+
+ /**
+ * If true, hide the mouse after typing a keystroke.
+ */
+ private boolean hideMouseWhenTyping = false;
+
+ /**
+ * If true, the mouse should not be displayed because a keystroke was
+ * typed.
+ */
+ private boolean typingHidMouse = false;
+
+ /**
+ * If true, hide the status bar.
+ */
+ private boolean hideStatusBar = false;
+
+ /**
+ * If true, hide the menu bar.
+ */
+ private boolean hideMenuBar = false;
+
+ /**
+ * The list of commands to run before the next I/O check.
+ */
+ private List<Runnable> invokeLaters = new LinkedList<Runnable>();
+
+ /**
+ * The last time the screen was resized.
+ */
+ private long screenResizeTime = 0;
+
+ /**
+ * WidgetEventHandler is the main event consumer loop. There are at most
+ * two such threads in existence: the primary for normal case and a
+ * secondary that is used for TMessageBox, TInputBox, and similar.
+ */
+ private class WidgetEventHandler implements Runnable {
+ /**
+ * The main application.
+ */
+ private TApplication application;
+
+ /**
+ * Whether or not this WidgetEventHandler is the primary or secondary
+ * thread.
+ */
+ private boolean primary = true;
+
+ /**
+ * Public constructor.
+ *
+ * @param application the main application
+ * @param primary if true, this is the primary event handler thread
+ */
+ public WidgetEventHandler(final TApplication application,
+ final boolean primary) {
+
+ this.application = application;
+ this.primary = primary;
+ }
+
+ /**
+ * The consumer loop.
+ */
+ public void run() {
+ // Wrap everything in a try, so that if we go belly up we can let
+ // the user have their terminal back.
+ try {
+ runImpl();
+ } catch (Throwable t) {
+ this.application.restoreConsole();
+ t.printStackTrace();
+ this.application.exit();
+ }
+ }
+
+ /**
+ * The consumer loop.
+ */
+ private void runImpl() {
+ boolean first = true;
+
+ // Loop forever
+ while (!application.quit) {
+
+ // Wait until application notifies me
+ while (!application.quit) {
+ try {
+ synchronized (application.drainEventQueue) {
+ if (application.drainEventQueue.size() > 0) {
+ break;
+ }
+ }
+
+ long timeout = 0;
+ if (first) {
+ first = false;
+ } else {
+ timeout = application.getSleepTime(1000);
+ }
+
+ if (timeout == 0) {
+ // A timer needs to fire, break out.
+ break;
+ }
+
+ if (debugThreads) {
+ System.err.printf("%d %s %s %s sleep %d millis\n",
+ System.currentTimeMillis(), this,
+ primary ? "primary" : "secondary",
+ Thread.currentThread(), timeout);
+ }
+
+ synchronized (this) {
+ this.wait(timeout);
+ }
+
+ if (debugThreads) {
+ System.err.printf("%d %s %s %s AWAKE\n",
+ System.currentTimeMillis(), this,
+ primary ? "primary" : "secondary",
+ Thread.currentThread());
+ }
+
+ if ((!primary)
+ && (application.secondaryEventReceiver == null)
+ ) {
+ // Secondary thread, emergency exit. If we got
+ // here then something went wrong with the
+ // handoff between yield() and closeWindow().
+ synchronized (application.primaryEventHandler) {
+ application.primaryEventHandler.notify();
+ }
+ application.secondaryEventHandler = null;
+ throw new RuntimeException("secondary exited " +
+ "at wrong time");
+ }
+ break;
+ } catch (InterruptedException e) {
+ // SQUASH
+ }
+ } // while (!application.quit)
+
+ // Pull all events off the queue
+ for (;;) {
+ TInputEvent event = null;
+ synchronized (application.drainEventQueue) {
+ if (application.drainEventQueue.size() == 0) {
+ break;
+ }
+ event = application.drainEventQueue.remove(0);
+ }
+
+ // We will have an event to process, so repaint the
+ // screen at the end.
+ application.repaint = true;
+
+ if (primary) {
+ primaryHandleEvent(event);
+ } else {
+ secondaryHandleEvent(event);
+ }
+ if ((!primary)
+ && (application.secondaryEventReceiver == null)
+ ) {
+ // Secondary thread, time to exit.
+
+ // Eliminate my reference so that wakeEventHandler()
+ // resumes working on the primary.
+ application.secondaryEventHandler = null;
+
+ // We are ready to exit, wake up the primary thread.
+ // Remember that it is currently sleeping inside its
+ // primaryHandleEvent().
+ synchronized (application.primaryEventHandler) {
+ application.primaryEventHandler.notify();
+ }
+
+ // All done!
+ return;
+ }
+
+ } // for (;;)
+
+ // Fire timers, update screen.
+ if (!quit) {
+ application.finishEventProcessing();
+ }
+
+ } // while (true) (main runnable loop)
+ }
+ }
+
+ /**
+ * ScreenHandler pushes screen updates to the physical device.
+ */
+ private class ScreenHandler implements Runnable {
+ /**
+ * The main application.
+ */
+ private TApplication application;
+
+ /**
+ * The dirty flag.
+ */
+ private boolean dirty = false;
+
+ /**
+ * Public constructor.
+ *
+ * @param application the main application
+ */
+ public ScreenHandler(final TApplication application) {
+ this.application = application;
+ }
+
+ /**
+ * The screen update loop.
+ */
+ public void run() {
+ // Wrap everything in a try, so that if we go belly up we can let
+ // the user have their terminal back.
+ try {
+ runImpl();
+ } catch (Throwable t) {
+ this.application.restoreConsole();
+ t.printStackTrace();
+ this.application.exit();
+ }
+ }
+
+ /**
+ * The update loop.
+ */
+ private void runImpl() {
+
+ // Loop forever
+ while (!application.quit) {
+
+ // Wait until application notifies me
+ while (!application.quit) {
+ try {
+ synchronized (this) {
+ if (dirty) {
+ dirty = false;
+ break;
+ }
+
+ // Always check within 50 milliseconds.
+ this.wait(50);
+ }
+ } catch (InterruptedException e) {
+ // SQUASH
+ }
+ } // while (!application.quit)
+
+ // Flush the screen contents
+ if (debugThreads) {
+ System.err.printf("%d %s backend.flushScreen()\n",
+ System.currentTimeMillis(), Thread.currentThread());
+ }
+ synchronized (getScreen()) {
+ backend.flushScreen();
+ }
+ } // while (true) (main runnable loop)
+
+ // Shutdown the user I/O thread(s)
+ backend.shutdown();
+ }
+
+ /**
+ * Set the dirty flag.
+ */
+ public void setDirty() {
+ synchronized (this) {
+ dirty = true;
+ }
+ }
+
+ }
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param backendType BackendType.XTERM, BackendType.ECMA48 or
+ * BackendType.SWING
+ * @param windowWidth the number of text columns to start with
+ * @param windowHeight the number of text rows to start with
+ * @param fontSize the size in points
+ * @throws UnsupportedEncodingException if an exception is thrown when
+ * creating the InputStreamReader
+ */
+ public TApplication(final BackendType backendType, final int windowWidth,
+ final int windowHeight, final int fontSize)
+ throws UnsupportedEncodingException {
+
+ switch (backendType) {
+ case SWING:
+ backend = new SwingBackend(this, windowWidth, windowHeight,
+ fontSize);
+ break;
+ case XTERM:
+ // Fall through...
+ case ECMA48:
+ backend = new ECMA48Backend(this, null, null, windowWidth,
+ windowHeight, fontSize);
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid backend type: "
+ + backendType);
+ }
+ TApplicationImpl();
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param backendType BackendType.XTERM, BackendType.ECMA48 or
+ * BackendType.SWING
+ * @throws UnsupportedEncodingException if an exception is thrown when
+ * creating the InputStreamReader
+ */
+ public TApplication(final BackendType backendType)
+ throws UnsupportedEncodingException {
+
+ switch (backendType) {
+ case SWING:
+ // The default SwingBackend is 80x25, 20 pt font. If you want to
+ // change that, you can pass the extra arguments to the
+ // SwingBackend constructor here. For example, if you wanted
+ // 90x30, 16 pt font:
+ //
+ // backend = new SwingBackend(this, 90, 30, 16);
+ backend = new SwingBackend(this);
+ break;
+ case XTERM:
+ // Fall through...
+ case ECMA48:
+ backend = new ECMA48Backend(this, null, null);
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid backend type: "
+ + backendType);
+ }
+ TApplicationImpl();
+ }
+
+ /**
+ * Public constructor. The backend type will be BackendType.ECMA48.
+ *
+ * @param input an InputStream connected to the remote user, or null for
+ * System.in. If System.in is used, then on non-Windows systems it will
+ * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
+ * mode. input is always converted to a Reader with UTF-8 encoding.
+ * @param output an OutputStream connected to the remote user, or null
+ * for System.out. output is always converted to a Writer with UTF-8
+ * encoding.
+ * @throws UnsupportedEncodingException if an exception is thrown when
+ * creating the InputStreamReader
+ */
+ public TApplication(final InputStream input,
+ final OutputStream output) throws UnsupportedEncodingException {
+
+ backend = new ECMA48Backend(this, input, output);
+ TApplicationImpl();
+ }
+
+ /**
+ * Public constructor. The backend type will be BackendType.ECMA48.
+ *
+ * @param input the InputStream underlying 'reader'. Its available()
+ * method is used to determine if reader.read() will block or not.
+ * @param reader a Reader connected to the remote user.
+ * @param writer a PrintWriter connected to the remote user.
+ * @param setRawMode if true, set System.in into raw mode with stty.
+ * This should in general not be used. It is here solely for Demo3,
+ * which uses System.in.
+ * @throws IllegalArgumentException if input, reader, or writer are null.
+ */
+ public TApplication(final InputStream input, final Reader reader,
+ final PrintWriter writer, final boolean setRawMode) {
+
+ backend = new ECMA48Backend(this, input, reader, writer, setRawMode);
+ TApplicationImpl();
+ }
+
+ /**
+ * Public constructor. The backend type will be BackendType.ECMA48.
+ *
+ * @param input the InputStream underlying 'reader'. Its available()
+ * method is used to determine if reader.read() will block or not.
+ * @param reader a Reader connected to the remote user.
+ * @param writer a PrintWriter connected to the remote user.
+ * @throws IllegalArgumentException if input, reader, or writer are null.
+ */
+ public TApplication(final InputStream input, final Reader reader,
+ final PrintWriter writer) {
+
+ this(input, reader, writer, false);
+ }
+
+ /**
+ * Public constructor. This hook enables use with new non-Jexer
+ * backends.
+ *
+ * @param backend a Backend that is already ready to go.
+ */
+ public TApplication(final Backend backend) {
+ this.backend = backend;
+ backend.setListener(this);
+ TApplicationImpl();
+ }
+
+ /**
+ * Finish construction once the backend is set.
+ */
+ private void TApplicationImpl() {
+ // Text block mouse option
+ if (System.getProperty("jexer.textMouse", "true").equals("false")) {
+ textMouse = false;
+ }
+
+ // Hide mouse when typing option
+ if (System.getProperty("jexer.hideMouseWhenTyping",
+ "false").equals("true")) {
+
+ hideMouseWhenTyping = true;
+ }
+
+ // Hide status bar option
+ if (System.getProperty("jexer.hideStatusBar",
+ "false").equals("true")) {
+ hideStatusBar = true;
+ }
+
+ // Hide menu bar option
+ if (System.getProperty("jexer.hideMenuBar", "false").equals("true")) {
+ hideMenuBar = true;
+ }
+
+ theme = new ColorTheme();
+ desktopTop = (hideMenuBar ? 0 : 1);
+ desktopBottom = getScreen().getHeight() - 1 + (hideStatusBar ? 1 : 0);
+ fillEventQueue = new LinkedList<TInputEvent>();
+ drainEventQueue = new LinkedList<TInputEvent>();
+ windows = new LinkedList<TWindow>();
+ menus = new ArrayList<TMenu>();
+ subMenus = new ArrayList<TMenu>();
+ timers = new LinkedList<TTimer>();
+ accelerators = new HashMap<TKeypress, TMenuItem>();
+ menuItems = new LinkedList<TMenuItem>();
+ desktop = new TDesktop(this);
+
+ // Special case: the Swing backend needs to have a timer to drive its
+ // blink state.
+ if ((backend instanceof SwingBackend)
+ || (backend instanceof MultiBackend)
+ ) {
+ // Default to 500 millis, unless a SwingBackend has its own
+ // value.
+ long millis = 500;
+ if (backend instanceof SwingBackend) {
+ millis = ((SwingBackend) backend).getBlinkMillis();
+ }
+ if (millis > 0) {
+ addTimer(millis, true,
+ new TAction() {
+ public void DO() {
+ TApplication.this.doRepaint();
+ }
+ }
+ );
+ }
+ }
+
+ }
+
+ // ------------------------------------------------------------------------
+ // Runnable ---------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Run this application until it exits.
+ */
+ public void run() {
+ // System.err.println("*** TApplication.run() begins ***");
+
+ // Start the screen updater thread
+ screenHandler = new ScreenHandler(this);
+ (new Thread(screenHandler)).start();
+
+ // Start the main consumer thread
+ primaryEventHandler = new WidgetEventHandler(this, true);
+ (new Thread(primaryEventHandler)).start();
+
+ started = true;
+
+ while (!quit) {
+ synchronized (this) {
+ boolean doWait = false;
+
+ if (!backend.hasEvents()) {
+ synchronized (fillEventQueue) {
+ if (fillEventQueue.size() == 0) {
+ doWait = true;
+ }
+ }
+ }
+
+ if (doWait) {
+ // No I/O to dispatch, so wait until the backend
+ // provides new I/O.
+ try {
+ if (debugThreads) {
+ System.err.println(System.currentTimeMillis() +
+ " " + Thread.currentThread() + " MAIN sleep");
+ }
+
+ this.wait();
+
+ if (debugThreads) {
+ System.err.println(System.currentTimeMillis() +
+ " " + Thread.currentThread() + " MAIN AWAKE");
+ }
+ } catch (InterruptedException e) {
+ // I'm awake and don't care why, let's see what's
+ // going on out there.
+ }
+ }
+
+ } // synchronized (this)
+
+ synchronized (fillEventQueue) {
+ // Pull any pending I/O events
+ backend.getEvents(fillEventQueue);
+
+ // Dispatch each event to the appropriate handler, one at a
+ // time.
+ for (;;) {
+ TInputEvent event = null;
+ if (fillEventQueue.size() == 0) {
+ break;
+ }
+ event = fillEventQueue.remove(0);
+ metaHandleEvent(event);
+ }
+ }
+
+ // Wake a consumer thread if we have any pending events.
+ if (drainEventQueue.size() > 0) {
+ wakeEventHandler();
+ }
+
+ } // while (!quit)
+
+ // Shutdown the event consumer threads
+ if (secondaryEventHandler != null) {
+ synchronized (secondaryEventHandler) {
+ secondaryEventHandler.notify();
+ }
+ }
+ if (primaryEventHandler != null) {
+ synchronized (primaryEventHandler) {
+ primaryEventHandler.notify();
+ }
+ }
+
+ // Close all the windows. This gives them an opportunity to release
+ // resources.
+ closeAllWindows();
+
+ // Close the desktop.
+ if (desktop != null) {
+ setDesktop(null);
+ }
+
+ // Give the overarching application an opportunity to release
+ // resources.
+ onExit();
+
+ // System.err.println("*** TApplication.run() exits ***");
+ }
+
+ // ------------------------------------------------------------------------
+ // Event handlers ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Method that TApplication subclasses can override to handle menu or
+ * posted command events.
+ *
+ * @param command command event
+ * @return if true, this event was consumed
+ */
+ protected boolean onCommand(final TCommandEvent command) {
+ // Default: handle cmExit
+ if (command.equals(cmExit)) {
+ if (messageBox(i18n.getString("exitDialogTitle"),
+ i18n.getString("exitDialogText"),
+ TMessageBox.Type.YESNO).isYes()) {
+
+ exit();
+ }
+ return true;
+ }
+
+ if (command.equals(cmShell)) {
+ openTerminal(0, 0, TWindow.RESIZABLE);
+ return true;
+ }
+
+ if (command.equals(cmTile)) {
+ tileWindows();
+ return true;
+ }
+ if (command.equals(cmCascade)) {
+ cascadeWindows();
+ return true;
+ }
+ if (command.equals(cmCloseAll)) {
+ closeAllWindows();
+ return true;
+ }
+
+ if (command.equals(cmMenu) && (hideMenuBar == false)) {
+ if (!modalWindowActive() && (activeMenu == null)) {
+ if (menus.size() > 0) {
+ menus.get(0).setActive(true);
+ activeMenu = menus.get(0);
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Method that TApplication subclasses can override to handle menu
+ * events.
+ *
+ * @param menu menu event
+ * @return if true, this event was consumed
+ */
+ protected boolean onMenu(final TMenuEvent menu) {
+
+ // Default: handle MID_EXIT
+ if (menu.getId() == TMenu.MID_EXIT) {
+ if (messageBox(i18n.getString("exitDialogTitle"),
+ i18n.getString("exitDialogText"),
+ TMessageBox.Type.YESNO).isYes()) {
+
+ exit();
+ }
+ return true;
+ }
+
+ if (menu.getId() == TMenu.MID_SHELL) {
+ openTerminal(0, 0, TWindow.RESIZABLE);
+ return true;
+ }
+
+ if (menu.getId() == TMenu.MID_TILE) {
+ tileWindows();
+ return true;
+ }
+ if (menu.getId() == TMenu.MID_CASCADE) {
+ cascadeWindows();
+ return true;
+ }
+ if (menu.getId() == TMenu.MID_CLOSE_ALL) {
+ closeAllWindows();
+ return true;
+ }
+ if (menu.getId() == TMenu.MID_ABOUT) {
+ showAboutDialog();
+ return true;
+ }
+ if (menu.getId() == TMenu.MID_REPAINT) {
+ getScreen().clearPhysical();
+ doRepaint();
+ return true;
+ }
+ if (menu.getId() == TMenu.MID_VIEW_IMAGE) {
+ openImage();
+ return true;
+ }
+ if (menu.getId() == TMenu.MID_SCREEN_OPTIONS) {
+ new TFontChooserWindow(this);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Method that TApplication subclasses can override to handle keystrokes.
+ *
+ * @param keypress keystroke event
+ * @return if true, this event was consumed
+ */
+ protected boolean onKeypress(final TKeypressEvent keypress) {
+ // Default: only menu shortcuts
+
+ // Process Alt-F, Alt-E, etc. menu shortcut keys
+ if (!keypress.getKey().isFnKey()
+ && keypress.getKey().isAlt()
+ && !keypress.getKey().isCtrl()
+ && (activeMenu == null)
+ && !modalWindowActive()
+ && (hideMenuBar == false)
+ ) {
+
+ assert (subMenus.size() == 0);
+
+ for (TMenu menu: menus) {
+ if (Character.toLowerCase(menu.getMnemonic().getShortcut())
+ == Character.toLowerCase(keypress.getKey().getChar())
+ ) {
+ activeMenu = menu;
+ menu.setActive(true);
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Process background events, and update the screen.
+ */
+ private void finishEventProcessing() {
+ if (debugThreads) {
+ System.err.printf(System.currentTimeMillis() + " " +
+ Thread.currentThread() + " finishEventProcessing()\n");
+ }
+
+ // Process timers and call doIdle()'s
+ doIdle();
+
+ // Update the screen
+ synchronized (getScreen()) {
+ drawAll();
+ }
+
+ // Wake up the screen repainter
+ wakeScreenHandler();
+
+ if (debugThreads) {
+ System.err.printf(System.currentTimeMillis() + " " +
+ Thread.currentThread() + " finishEventProcessing() END\n");
+ }
+ }
+
+ /**
+ * Peek at certain application-level events, add to eventQueue, and wake
+ * up the consuming Thread.
+ *
+ * @param event the input event to consume
+ */
+ private void metaHandleEvent(final TInputEvent event) {
+
+ if (debugEvents) {
+ System.err.printf(String.format("metaHandleEvents event: %s\n",
+ event)); System.err.flush();
+ }
+
+ if (quit) {
+ // Do no more processing if the application is already trying
+ // to exit.
+ return;
+ }
+
+ // Special application-wide events -------------------------------
+
+ // Abort everything
+ if (event instanceof TCommandEvent) {
+ TCommandEvent command = (TCommandEvent) event;
+ if (command.equals(cmAbort)) {
+ exit();
+ return;
+ }
+ }
+
+ synchronized (drainEventQueue) {
+ // Screen resize
+ if (event instanceof TResizeEvent) {
+ TResizeEvent resize = (TResizeEvent) event;
+ synchronized (getScreen()) {
+ if ((System.currentTimeMillis() - screenResizeTime >= 15)
+ || (resize.getWidth() < getScreen().getWidth())
+ || (resize.getHeight() < getScreen().getHeight())
+ ) {
+ getScreen().setDimensions(resize.getWidth(),
+ resize.getHeight());
+ screenResizeTime = System.currentTimeMillis();
+ }
+ desktopBottom = getScreen().getHeight() - 1;
+ if (hideStatusBar) {
+ desktopBottom++;
+ }
+ mouseX = 0;
+ mouseY = 0;
+ oldMouseX = 0;
+ oldMouseY = 0;
+ }
+ if (desktop != null) {
+ desktop.setDimensions(0, desktopTop, resize.getWidth(),
+ (desktopBottom - desktopTop));
+ desktop.onResize(resize);
+ }
+
+ // Change menu edges if needed.
+ recomputeMenuX();
+
+ // We are dirty, redraw the screen.
+ doRepaint();
+
+ /*
+ System.err.println("New screen: " + resize.getWidth() +
+ " x " + resize.getHeight());
+ */
+ return;
+ }
+
+ // Put into the main queue
+ drainEventQueue.add(event);
+ }
+ }
+
+ /**
+ * Dispatch one event to the appropriate widget or application-level
+ * event handler. This is the primary event handler, it has the normal
+ * application-wide event handling.
+ *
+ * @param event the input event to consume
+ * @see #secondaryHandleEvent(TInputEvent event)
+ */
+ private void primaryHandleEvent(final TInputEvent event) {
+
+ if (debugEvents) {
+ System.err.printf("%s primaryHandleEvent: %s\n",
+ Thread.currentThread(), event);
+ }
+ TMouseEvent doubleClick = null;
+
+ // Special application-wide events -----------------------------------
+
+ if (event instanceof TKeypressEvent) {
+ if (hideMouseWhenTyping) {
+ typingHidMouse = true;
+ }
+ }
+
+ // Peek at the mouse position
+ if (event instanceof TMouseEvent) {
+ typingHidMouse = false;
+
+ TMouseEvent mouse = (TMouseEvent) event;
+ if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) {
+ oldMouseX = mouseX;
+ oldMouseY = mouseY;
+ mouseX = mouse.getX();
+ mouseY = mouse.getY();
+ } else {
+ if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN)
+ && (!mouse.isMouseWheelUp())
+ && (!mouse.isMouseWheelDown())
+ ) {
+ if ((mouse.getTime().getTime() - lastMouseUpTime) <
+ doubleClickTime) {
+
+ // This is a double-click.
+ doubleClick = new TMouseEvent(TMouseEvent.Type.
+ MOUSE_DOUBLE_CLICK,
+ mouse.getX(), mouse.getY(),
+ mouse.getAbsoluteX(), mouse.getAbsoluteY(),
+ mouse.isMouse1(), mouse.isMouse2(),
+ mouse.isMouse3(),
+ mouse.isMouseWheelUp(), mouse.isMouseWheelDown());
+
+ } else {
+ // The first click of a potential double-click.
+ lastMouseUpTime = mouse.getTime().getTime();
+ }
+ }
+ }
+
+ // See if we need to switch focus to another window or the menu
+ checkSwitchFocus((TMouseEvent) event);
+ }
+
+ // Handle menu events
+ if ((activeMenu != null) && !(event instanceof TCommandEvent)) {
+ TMenu menu = activeMenu;
+
+ if (event instanceof TMouseEvent) {
+ TMouseEvent mouse = (TMouseEvent) event;
+
+ while (subMenus.size() > 0) {
+ TMenu subMenu = subMenus.get(subMenus.size() - 1);
+ if (subMenu.mouseWouldHit(mouse)) {
+ break;
+ }
+ if ((mouse.getType() == TMouseEvent.Type.MOUSE_MOTION)
+ && (!mouse.isMouse1())
+ && (!mouse.isMouse2())
+ && (!mouse.isMouse3())
+ && (!mouse.isMouseWheelUp())
+ && (!mouse.isMouseWheelDown())
+ ) {
+ break;
+ }
+ // We navigated away from a sub-menu, so close it
+ closeSubMenu();
+ }
+
+ // Convert the mouse relative x/y to menu coordinates
+ assert (mouse.getX() == mouse.getAbsoluteX());
+ assert (mouse.getY() == mouse.getAbsoluteY());
+ if (subMenus.size() > 0) {
+ menu = subMenus.get(subMenus.size() - 1);
+ }
+ mouse.setX(mouse.getX() - menu.getX());
+ mouse.setY(mouse.getY() - menu.getY());
+ }
+ menu.handleEvent(event);
+ return;
+ }
+
+ if (event instanceof TKeypressEvent) {
+ TKeypressEvent keypress = (TKeypressEvent) event;
+
+ // See if this key matches an accelerator, and is not being
+ // shortcutted by the active window, and if so dispatch the menu
+ // event.
+ boolean windowWillShortcut = false;
+ if (activeWindow != null) {
+ assert (activeWindow.isShown());
+ if (activeWindow.isShortcutKeypress(keypress.getKey())) {
+ // We do not process this key, it will be passed to the
+ // window instead.
+ windowWillShortcut = true;
+ }
+ }
+
+ if (!windowWillShortcut && !modalWindowActive()) {
+ TKeypress keypressLowercase = keypress.getKey().toLowerCase();
+ TMenuItem item = null;
+ synchronized (accelerators) {
+ item = accelerators.get(keypressLowercase);
+ }
+ if (item != null) {
+ if (item.isEnabled()) {
+ // Let the menu item dispatch
+ item.dispatch();
+ return;
+ }
+ }
+
+ // Handle the keypress
+ if (onKeypress(keypress)) {
+ return;
+ }
+ }
+ }
+
+ if (event instanceof TCommandEvent) {
+ if (onCommand((TCommandEvent) event)) {
+ return;
+ }
+ }
+
+ if (event instanceof TMenuEvent) {
+ if (onMenu((TMenuEvent) event)) {
+ return;
+ }
+ }
+
+ // Dispatch events to the active window -------------------------------
+ boolean dispatchToDesktop = true;
+ TWindow window = activeWindow;
+ if (window != null) {
+ assert (window.isActive());
+ assert (window.isShown());
+ if (event instanceof TMouseEvent) {
+ TMouseEvent mouse = (TMouseEvent) event;
+ // Convert the mouse relative x/y to window coordinates
+ assert (mouse.getX() == mouse.getAbsoluteX());
+ assert (mouse.getY() == mouse.getAbsoluteY());
+ mouse.setX(mouse.getX() - window.getX());
+ mouse.setY(mouse.getY() - window.getY());
+
+ if (doubleClick != null) {
+ doubleClick.setX(doubleClick.getX() - window.getX());
+ doubleClick.setY(doubleClick.getY() - window.getY());
+ }
+
+ if (window.mouseWouldHit(mouse)) {
+ dispatchToDesktop = false;
+ }
+ } else if (event instanceof TKeypressEvent) {
+ dispatchToDesktop = false;
+ } else if (event instanceof TMenuEvent) {
+ dispatchToDesktop = false;
+ }
+
+ if (debugEvents) {
+ System.err.printf("TApplication dispatch event: %s\n",
+ event);
+ }
+ window.handleEvent(event);
+ if (doubleClick != null) {
+ window.handleEvent(doubleClick);
+ }
+ }
+ if (dispatchToDesktop) {
+ // This event is fair game for the desktop to process.
+ if (desktop != null) {
+ desktop.handleEvent(event);
+ if (doubleClick != null) {
+ desktop.handleEvent(doubleClick);
+ }
+ }
+ }
+ }
+
+ /**
+ * Dispatch one event to the appropriate widget or application-level
+ * event handler. This is the secondary event handler used by certain
+ * special dialogs (currently TMessageBox and TFileOpenBox).
+ *
+ * @param event the input event to consume
+ * @see #primaryHandleEvent(TInputEvent event)
+ */
+ private void secondaryHandleEvent(final TInputEvent event) {
+ TMouseEvent doubleClick = null;
+
+ if (debugEvents) {
+ System.err.printf("%s secondaryHandleEvent: %s\n",
+ Thread.currentThread(), event);
+ }
+
+ // Peek at the mouse position
+ if (event instanceof TMouseEvent) {
+ typingHidMouse = false;
+
+ TMouseEvent mouse = (TMouseEvent) event;
+ if ((mouseX != mouse.getX()) || (mouseY != mouse.getY())) {
+ oldMouseX = mouseX;
+ oldMouseY = mouseY;
+ mouseX = mouse.getX();
+ mouseY = mouse.getY();
+ } else {
+ if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN)
+ && (!mouse.isMouseWheelUp())
+ && (!mouse.isMouseWheelDown())
+ ) {
+ if ((mouse.getTime().getTime() - lastMouseUpTime) <
+ doubleClickTime) {
+
+ // This is a double-click.
+ doubleClick = new TMouseEvent(TMouseEvent.Type.
+ MOUSE_DOUBLE_CLICK,
+ mouse.getX(), mouse.getY(),
+ mouse.getAbsoluteX(), mouse.getAbsoluteY(),
+ mouse.isMouse1(), mouse.isMouse2(),
+ mouse.isMouse3(),
+ mouse.isMouseWheelUp(), mouse.isMouseWheelDown());
+
+ } else {
+ // The first click of a potential double-click.
+ lastMouseUpTime = mouse.getTime().getTime();
+ }
+ }
+ }
+ }
+
+ secondaryEventReceiver.handleEvent(event);
+ // Note that it is possible for secondaryEventReceiver to be null
+ // now, because its handleEvent() might have finished out on the
+ // secondary thread. So put any extra processing inside a null
+ // check.
+ if (secondaryEventReceiver != null) {
+ if (doubleClick != null) {
+ secondaryEventReceiver.handleEvent(doubleClick);
+ }
+ }
+ }
+
+ /**
+ * Enable a widget to override the primary event thread.
+ *
+ * @param widget widget that will receive events
+ */
+ public final void enableSecondaryEventReceiver(final TWidget widget) {
+ if (debugThreads) {
+ System.err.println(System.currentTimeMillis() +
+ " enableSecondaryEventReceiver()");
+ }
+
+ assert (secondaryEventReceiver == null);
+ assert (secondaryEventHandler == null);
+ assert ((widget instanceof TMessageBox)
+ || (widget instanceof TFileOpenBox));
+ secondaryEventReceiver = widget;
+ secondaryEventHandler = new WidgetEventHandler(this, false);
+
+ (new Thread(secondaryEventHandler)).start();
+ }
+
+ /**
+ * Yield to the secondary thread.
+ */
+ public final void yield() {
+ if (debugThreads) {
+ System.err.printf(System.currentTimeMillis() + " " +
+ Thread.currentThread() + " yield()\n");
+ }
+
+ assert (secondaryEventReceiver != null);
+
+ while (secondaryEventReceiver != null) {
+ synchronized (primaryEventHandler) {
+ try {
+ primaryEventHandler.wait();
+ } catch (InterruptedException e) {
+ // SQUASH
+ }
+ }
+ }
+ }
+
+ /**
+ * Do stuff when there is no user input.
+ */
+ private void doIdle() {
+ if (debugThreads) {
+ System.err.printf(System.currentTimeMillis() + " " +
+ Thread.currentThread() + " doIdle()\n");
+ }
+
+ synchronized (timers) {
+
+ if (debugThreads) {
+ System.err.printf(System.currentTimeMillis() + " " +
+ Thread.currentThread() + " doIdle() 2\n");
+ }
+
+ // Run any timers that have timed out
+ Date now = new Date();
+ List<TTimer> keepTimers = new LinkedList<TTimer>();
+ for (TTimer timer: timers) {
+ if (timer.getNextTick().getTime() <= now.getTime()) {
+ // Something might change, so repaint the screen.
+ repaint = true;
+ timer.tick();
+ if (timer.recurring) {
+ keepTimers.add(timer);
+ }
+ } else {
+ keepTimers.add(timer);
+ }
+ }
+ timers.clear();
+ timers.addAll(keepTimers);
+ }
+
+ // Call onIdle's
+ for (TWindow window: windows) {
+ window.onIdle();
+ }
+ if (desktop != null) {
+ desktop.onIdle();
+ }
+
+ // Run any invokeLaters
+ synchronized (invokeLaters) {
+ for (Runnable invoke: invokeLaters) {
+ invoke.run();
+ }
+ invokeLaters.clear();
+ }
+
+ }
+
+ /**
+ * Wake the sleeping active event handler.
+ */
+ private void wakeEventHandler() {
+ if (!started) {
+ return;
+ }
+
+ if (secondaryEventHandler != null) {
+ synchronized (secondaryEventHandler) {
+ secondaryEventHandler.notify();
+ }
+ } else {
+ assert (primaryEventHandler != null);
+ synchronized (primaryEventHandler) {
+ primaryEventHandler.notify();
+ }
+ }
+ }
+
+ /**
+ * Wake the sleeping screen handler.
+ */
+ private void wakeScreenHandler() {
+ if (!started) {
+ return;
+ }
+
+ synchronized (screenHandler) {
+ screenHandler.notify();
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // TApplication -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Place a command on the run queue, and run it before the next round of
+ * checking I/O.
+ *
+ * @param command the command to run later
+ */
+ public void invokeLater(final Runnable command) {
+ synchronized (invokeLaters) {
+ invokeLaters.add(command);
+ }
+ doRepaint();
+ }
+
+ /**
+ * Restore the console to sane defaults. This is meant to be used for
+ * improper exits (e.g. a caught exception in main()), and should not be
+ * necessary for normal program termination.
+ */
+ public void restoreConsole() {
+ if (backend != null) {
+ if (backend instanceof ECMA48Backend) {
+ backend.shutdown();
+ }
+ }
+ }
+
+ /**
+ * Get the Backend.
+ *
+ * @return the Backend
+ */
+ public final Backend getBackend() {
+ return backend;
+ }
+
+ /**
+ * Get the Screen.
+ *
+ * @return the Screen
+ */
+ public final Screen getScreen() {
+ if (backend instanceof TWindowBackend) {
+ // We are being rendered to a TWindow. We can't use its
+ // getScreen() method because that is how it is rendering to a
+ // hardware backend somewhere. Instead use its getOtherScreen()
+ // method.
+ return ((TWindowBackend) backend).getOtherScreen();
+ } else {
+ return backend.getScreen();
+ }
+ }
+
+ /**
+ * Get the color theme.
+ *
+ * @return the theme
+ */
+ public final ColorTheme getTheme() {
+ return theme;
+ }
+
+ /**
+ * Repaint the screen on the next update.
+ */
+ public void doRepaint() {
+ repaint = true;
+ wakeEventHandler();
+ }
+
+ /**
+ * Get Y coordinate of the top edge of the desktop.
+ *
+ * @return Y coordinate of the top edge of the desktop
+ */
+ public final int getDesktopTop() {
+ return desktopTop;
+ }
+
+ /**
+ * Get Y coordinate of the bottom edge of the desktop.
+ *
+ * @return Y coordinate of the bottom edge of the desktop
+ */
+ public final int getDesktopBottom() {
+ return desktopBottom;
+ }
+
+ /**
+ * Set the TDesktop instance.
+ *
+ * @param desktop a TDesktop instance, or null to remove the one that is
+ * set
+ */
+ public final void setDesktop(final TDesktop desktop) {
+ if (this.desktop != null) {
+ this.desktop.onPreClose();
+ this.desktop.onUnfocus();
+ this.desktop.onClose();
+ }
+ this.desktop = desktop;
+ }
+
+ /**
+ * Get the TDesktop instance.
+ *
+ * @return the desktop, or null if it is not set
+ */
+ public final TDesktop getDesktop() {
+ return desktop;
+ }
+
+ /**
+ * Get the current active window.
+ *
+ * @return the active window, or null if it is not set
+ */
+ public final TWindow getActiveWindow() {
+ return activeWindow;
+ }
+
+ /**
+ * Get a (shallow) copy of the window list.
+ *
+ * @return a copy of the list of windows for this application
+ */
+ public final List<TWindow> getAllWindows() {
+ List<TWindow> result = new ArrayList<TWindow>();
+ result.addAll(windows);
+ return result;
+ }
+
+ /**
+ * Get focusFollowsMouse flag.
+ *
+ * @return true if focus follows mouse: windows automatically raised if
+ * the mouse passes over them
+ */
+ public boolean getFocusFollowsMouse() {
+ return focusFollowsMouse;
+ }
+
+ /**
+ * Set focusFollowsMouse flag.
+ *
+ * @param focusFollowsMouse if true, focus follows mouse: windows
+ * automatically raised if the mouse passes over them
+ */
+ public void setFocusFollowsMouse(final boolean focusFollowsMouse) {
+ this.focusFollowsMouse = focusFollowsMouse;
+ }
+
+ /**
+ * Display the about dialog.
+ */
+ protected void showAboutDialog() {
+ String version = getClass().getPackage().getImplementationVersion();
+ if (version == null) {
+ // This is Java 9+, use a hardcoded string here.
+ version = "0.3.2";
+ }
+ messageBox(i18n.getString("aboutDialogTitle"),
+ MessageFormat.format(i18n.getString("aboutDialogText"), version),
+ TMessageBox.Type.OK);
+ }
+
+ /**
+ * Handle the Tool | Open image menu item.
+ */
+ private void openImage() {
+ try {
+ List<String> filters = new ArrayList<String>();
+ filters.add("^.*\\.[Jj][Pp][Gg]$");
+ filters.add("^.*\\.[Jj][Pp][Ee][Gg]$");
+ filters.add("^.*\\.[Pp][Nn][Gg]$");
+ filters.add("^.*\\.[Gg][Ii][Ff]$");
+ filters.add("^.*\\.[Bb][Mm][Pp]$");
+ String filename = fileOpenBox(".", TFileOpenBox.Type.OPEN, filters);
+ if (filename != null) {
+ new TImageWindow(this, new File(filename));
+ }
+ } catch (IOException e) {
+ // Show this exception to the user.
+ new TExceptionDialog(this, e);
+ }
+ }
+
+ /**
+ * Check if application is still running.
+ *
+ * @return true if the application is running
+ */
+ public final boolean isRunning() {
+ if (quit == true) {
+ return false;
+ }
+ return true;
+ }
+
+ // ------------------------------------------------------------------------
+ // Screen refresh loop ----------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * 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.
+ *
+ * @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) {
+
+ if (debugThreads) {
+ System.err.printf("%d %s invertCell() %d %d\n",
+ System.currentTimeMillis(), Thread.currentThread(), x, y);
+
+ if (activeWindow != null) {
+ System.err.println("activeWindow.hasHiddenMouse() " +
+ activeWindow.hasHiddenMouse());
+ }
+ }
+
+ // If this cell is on top of a visible window that has requested a
+ // hidden mouse, bail out.
+ if ((activeWindow != null) && (activeMenu == null)) {
+ if ((activeWindow.hasHiddenMouse() == true)
+ && (x > activeWindow.getX())
+ && (x < activeWindow.getX() + activeWindow.getWidth() - 1)
+ && (y > activeWindow.getY())
+ && (y < activeWindow.getY() + activeWindow.getHeight() - 1)
+ ) {
+ return;
+ }
+ }
+
+ // If this cell is on top of the desktop, and the desktop has
+ // requested a hidden mouse, bail out.
+ if ((desktop != null) && (activeWindow == null) && (activeMenu == null)) {
+ if ((desktop.hasHiddenMouse() == true)
+ && (x > desktop.getX())
+ && (x < desktop.getX() + desktop.getWidth() - 1)
+ && (y > desktop.getY())
+ && (y < desktop.getY() + desktop.getHeight() - 1)
+ ) {
+ return;
+ }
+ }
+
+ 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);
+ }
+ }
+ }
+ }
+
+ /**
+ * Draw everything.
+ */
+ private void drawAll() {
+ boolean menuIsActive = false;
+
+ if (debugThreads) {
+ System.err.printf("%d %s drawAll() enter\n",
+ System.currentTimeMillis(), Thread.currentThread());
+ }
+
+ // I don't think this does anything useful anymore...
+ if (!repaint) {
+ if (debugThreads) {
+ System.err.printf("%d %s drawAll() !repaint\n",
+ System.currentTimeMillis(), Thread.currentThread());
+ }
+ if ((oldDrawnMouseX != mouseX) || (oldDrawnMouseY != mouseY)) {
+ if (debugThreads) {
+ System.err.printf("%d %s drawAll() !repaint MOUSE\n",
+ System.currentTimeMillis(), Thread.currentThread());
+ }
+
+ // The only thing that has happened is the mouse moved.
+
+ // Redraw the old cell at that position, and save the cell at
+ // the new mouse position.
+ if (debugThreads) {
+ System.err.printf("%d %s restoreImage() %d %d\n",
+ System.currentTimeMillis(), Thread.currentThread(),
+ oldDrawnMouseX, oldDrawnMouseY);
+ }
+ oldDrawnMouseCell.restoreImage();
+ getScreen().putCharXY(oldDrawnMouseX, oldDrawnMouseY,
+ oldDrawnMouseCell);
+ oldDrawnMouseCell = getScreen().getCharXY(mouseX, mouseY);
+ if (backend instanceof ECMA48Backend) {
+ // Special case: the entire row containing the mouse has
+ // to be re-drawn if it has any image data, AND any rows
+ // in between.
+ if (oldDrawnMouseY != mouseY) {
+ for (int i = oldDrawnMouseY; ;) {
+ getScreen().unsetImageRow(i);
+ if (i == mouseY) {
+ break;
+ }
+ if (oldDrawnMouseY < mouseY) {
+ i++;
+ } else {
+ i--;
+ }
+ }
+ } else {
+ getScreen().unsetImageRow(mouseY);
+ }
+ }
+
+ if ((textMouse == true) && (typingHidMouse == false)) {
+ // Draw mouse at the new position.
+ invertCell(mouseX, mouseY);
+ }
+
+ oldDrawnMouseX = mouseX;
+ oldDrawnMouseY = mouseY;
+ }
+ if (getScreen().isDirty()) {
+ screenHandler.setDirty();
+ }
+ return;
+ }
+
+ if (debugThreads) {
+ System.err.printf("%d %s drawAll() REDRAW\n",
+ System.currentTimeMillis(), Thread.currentThread());
+ }
+
+ // If true, the cursor is not visible
+ boolean cursor = false;
+
+ // Start with a clean screen
+ getScreen().clear();
+
+ // Draw the desktop
+ if (desktop != null) {
+ desktop.drawChildren();
+ }
+
+ // Draw each window in reverse Z order
+ List<TWindow> sorted = new ArrayList<TWindow>(windows);
+ Collections.sort(sorted);
+ TWindow topLevel = null;
+ if (sorted.size() > 0) {
+ topLevel = sorted.get(0);
+ }
+ Collections.reverse(sorted);
+ for (TWindow window: sorted) {
+ if (window.isShown()) {
+ window.drawChildren();
+ }
+ }
+
+ if (hideMenuBar == false) {
+
+ // Draw the blank menubar line - reset the screen clipping first
+ // so it won't trim it out.
+ getScreen().resetClipping();
+ getScreen().hLineXY(0, 0, getScreen().getWidth(), ' ',
+ theme.getColor("tmenu"));
+ // Now draw the menus.
+ int x = 1;
+ for (TMenu menu: menus) {
+ CellAttributes menuColor;
+ CellAttributes menuMnemonicColor;
+ if (menu.isActive()) {
+ menuIsActive = true;
+ menuColor = theme.getColor("tmenu.highlighted");
+ menuMnemonicColor = theme.getColor("tmenu.mnemonic.highlighted");
+ topLevel = menu;
+ } else {
+ menuColor = theme.getColor("tmenu");
+ menuMnemonicColor = theme.getColor("tmenu.mnemonic");
+ }
+ // Draw the menu title
+ getScreen().hLineXY(x, 0,
+ StringUtils.width(menu.getTitle()) + 2, ' ', menuColor);
+ getScreen().putStringXY(x + 1, 0, menu.getTitle(), menuColor);
+ // Draw the highlight character
+ getScreen().putCharXY(x + 1 +
+ menu.getMnemonic().getScreenShortcutIdx(),
+ 0, menu.getMnemonic().getShortcut(), menuMnemonicColor);
+
+ if (menu.isActive()) {
+ ((TWindow) menu).drawChildren();
+ // Reset the screen clipping so we can draw the next
+ // title.
+ getScreen().resetClipping();
+ }
+ x += StringUtils.width(menu.getTitle()) + 2;
+ }
+
+ for (TMenu menu: subMenus) {
+ // Reset the screen clipping so we can draw the next
+ // sub-menu.
+ getScreen().resetClipping();
+ ((TWindow) menu).drawChildren();
+ }
+ }
+ getScreen().resetClipping();
+
+ if (hideStatusBar == false) {
+ // Draw the status bar of the top-level window
+ TStatusBar statusBar = null;
+ if (topLevel != null) {
+ statusBar = topLevel.getStatusBar();
+ }
+ if (statusBar != null) {
+ getScreen().resetClipping();
+ statusBar.setWidth(getScreen().getWidth());
+ statusBar.setY(getScreen().getHeight() - topLevel.getY());
+ statusBar.draw();
+ } else {
+ CellAttributes barColor = new CellAttributes();
+ barColor.setTo(getTheme().getColor("tstatusbar.text"));
+ getScreen().hLineXY(0, desktopBottom, getScreen().getWidth(),
+ ' ', barColor);
+ }
+ }
+
+ // Draw the mouse pointer
+ if (debugThreads) {
+ System.err.printf("%d %s restoreImage() %d %d\n",
+ System.currentTimeMillis(), Thread.currentThread(),
+ oldDrawnMouseX, oldDrawnMouseY);
+ }
+ oldDrawnMouseCell = getScreen().getCharXY(mouseX, mouseY);
+ if (backend instanceof ECMA48Backend) {
+ // Special case: the entire row containing the mouse has to be
+ // re-drawn if it has any image data, AND any rows in between.
+ if (oldDrawnMouseY != mouseY) {
+ for (int i = oldDrawnMouseY; ;) {
+ getScreen().unsetImageRow(i);
+ if (i == mouseY) {
+ break;
+ }
+ if (oldDrawnMouseY < mouseY) {
+ i++;
+ } else {
+ i--;
+ }
+ }
+ } else {
+ getScreen().unsetImageRow(mouseY);
+ }
+ }
+ if ((textMouse == true) && (typingHidMouse == false)) {
+ invertCell(mouseX, mouseY);
+ }
+ oldDrawnMouseX = mouseX;
+ oldDrawnMouseY = mouseY;
+
+ // Place the cursor if it is visible
+ if (!menuIsActive) {
+
+ int visibleWindowCount = 0;
+ for (TWindow window: sorted) {
+ if (window.isShown()) {
+ visibleWindowCount++;
+ }
+ }
+ if (visibleWindowCount == 0) {
+ // No windows are visible, only the desktop. Allow it to
+ // have the cursor.
+ if (desktop != null) {
+ sorted.add(desktop);
+ }
+ }
+
+ TWidget activeWidget = null;
+ if (sorted.size() > 0) {
+ activeWidget = sorted.get(sorted.size() - 1).getActiveChild();
+ int cursorClipTop = desktopTop;
+ int cursorClipBottom = desktopBottom;
+ if (activeWidget.isCursorVisible()) {
+ if ((activeWidget.getCursorAbsoluteY() <= cursorClipBottom)
+ && (activeWidget.getCursorAbsoluteY() >= cursorClipTop)
+ ) {
+ getScreen().putCursor(true,
+ activeWidget.getCursorAbsoluteX(),
+ activeWidget.getCursorAbsoluteY());
+ cursor = true;
+ } else {
+ // Turn off the cursor. Also place it at 0,0.
+ getScreen().putCursor(false, 0, 0);
+ cursor = false;
+ }
+ }
+ }
+ }
+
+ // Kill the cursor
+ if (!cursor) {
+ getScreen().hideCursor();
+ }
+
+ if (getScreen().isDirty()) {
+ screenHandler.setDirty();
+ }
+ repaint = false;
+ }
+
+ /**
+ * Force this application to exit.
+ */
+ public void exit() {
+ quit = true;
+ synchronized (this) {
+ this.notify();
+ }
+ }
+
+ /**
+ * Subclasses can use this hook to cleanup resources. Called as the last
+ * step of TApplication.run().
+ */
+ public void onExit() {
+ // Default does nothing.
+ }
+
+ // ------------------------------------------------------------------------
+ // TWindow management -----------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Return the total number of windows.
+ *
+ * @return the total number of windows
+ */
+ public final int windowCount() {
+ return windows.size();
+ }
+
+ /**
+ * Return the number of windows that are showing.
+ *
+ * @return the number of windows that are showing on screen
+ */
+ public final int shownWindowCount() {
+ int n = 0;
+ for (TWindow w: windows) {
+ if (w.isShown()) {
+ n++;
+ }
+ }
+ return n;
+ }
+
+ /**
+ * Return the number of windows that are hidden.
+ *
+ * @return the number of windows that are hidden
+ */
+ public final int hiddenWindowCount() {
+ int n = 0;
+ for (TWindow w: windows) {
+ if (w.isHidden()) {
+ n++;
+ }
+ }
+ return n;
+ }
+
+ /**
+ * Check if a window instance is in this application's window list.
+ *
+ * @param window window to look for
+ * @return true if this window is in the list
+ */
+ public final boolean hasWindow(final TWindow window) {
+ if (windows.size() == 0) {
+ return false;
+ }
+ for (TWindow w: windows) {
+ if (w == window) {
+ assert (window.getApplication() == this);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Activate a window: bring it to the top and have it receive events.
+ *
+ * @param window the window to become the new active window
+ */
+ public void activateWindow(final TWindow window) {
+ if (hasWindow(window) == false) {
+ /*
+ * Someone has a handle to a window I don't have. Ignore this
+ * request.
+ */
+ 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.isHidden()) {
+ // Unhiding will also activate.
+ showWindow(window);
+ return;
+ }
+ assert (window.isShown());
+
+ if (windows.size() == 1) {
+ assert (window == windows.get(0));
+ if (activeWindow == null) {
+ activeWindow = window;
+ window.setZ(0);
+ activeWindow.setActive(true);
+ activeWindow.onFocus();
+ }
+
+ assert (window.isActive());
+ assert (activeWindow == window);
+ return;
+ }
+
+ if (activeWindow == window) {
+ assert (window.isActive());
+
+ // Window is already active, do nothing.
+ return;
+ }
+
+ assert (!window.isActive());
+ if (activeWindow != null) {
+ activeWindow.setActive(false);
+
+ // Increment every window Z that is on top of window
+ for (TWindow w: windows) {
+ if (w == window) {
+ continue;
+ }
+ if (w.getZ() < window.getZ()) {
+ w.setZ(w.getZ() + 1);
+ }
+ }
+
+ // 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;
+ }
+
+ /**
+ * Hide a window.
+ *
+ * @param window the window to hide
+ */
+ public void hideWindow(final TWindow window) {
+ if (hasWindow(window) == false) {
+ /*
+ * Someone has a handle to a window I don't have. Ignore this
+ * request.
+ */
+ 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) {
+ if (window == activeWindow) {
+ if (shownWindowCount() > 1) {
+ switchWindow(true);
+ } else {
+ activeWindow = null;
+ window.setActive(false);
+ window.onUnfocus();
+ }
+ }
+ window.hidden = true;
+ window.onHide();
+ }
+ }
+
+ /**
+ * Show a window.
+ *
+ * @param window the window to show
+ */
+ public void showWindow(final TWindow window) {
+ if (hasWindow(window) == false) {
+ /*
+ * Someone has a handle to a window I don't have. Ignore this
+ * request.
+ */
+ 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.
+ *
+ * @param window the window to remove
+ */
+ public final void closeWindow(final TWindow window) {
+ if (hasWindow(window) == false) {
+ /*
+ * Someone has a handle to a window I don't have. Ignore this
+ * request.
+ */
+ return;
+ }
+
+ // Let window know that it is about to be closed, while it is still
+ // visible on screen.
+ 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.onUnfocus();
+ windows.remove(window);
+ Collections.sort(windows);
+ activeWindow = null;
+ int newZ = 0;
+ boolean foundNextWindow = false;
+
+ for (TWindow w: windows) {
+ w.setZ(newZ);
+ newZ++;
+
+ // Do not activate a hidden window.
+ if (w.isHidden()) {
+ continue;
+ }
+
+ if (foundNextWindow == false) {
+ foundNextWindow = true;
+ w.setActive(true);
+ w.onFocus();
+ assert (activeWindow == null);
+ activeWindow = w;
+ continue;
+ }
+
+ if (w.isActive()) {
+ w.setActive(false);
+ w.onUnfocus();
+ }
+ }
+ }
+
+ // Perform window cleanup
+ window.onClose();
+
+ // Check if we are closing a TMessageBox or similar
+ if (secondaryEventReceiver != null) {
+ assert (secondaryEventHandler != null);
+
+ // Do not send events to the secondaryEventReceiver anymore, the
+ // window is closed.
+ secondaryEventReceiver = null;
+
+ // Wake the secondary thread, it will wake the primary as it
+ // exits.
+ synchronized (secondaryEventHandler) {
+ secondaryEventHandler.notify();
+ }
+ }
+
+ // Permit desktop to be active if it is the only thing left.
+ if (desktop != null) {
+ if (windows.size() == 0) {
+ desktop.setActive(true);
+ }
+ }
+ }
+
+ /**
+ * Switch to the next window.
+ *
+ * @param forward if true, then switch to the next window in the list,
+ * otherwise switch to the previous window in the list
+ */
+ public final void switchWindow(final boolean forward) {
+ // Only switch if there are multiple visible windows
+ if (shownWindowCount() < 2) {
+ return;
+ }
+ assert (activeWindow != null);
+
+ 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;
+ } else {
+ assert (!windows.get(0).isActive());
+ }
+ }
+ 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;
+ }
+ }
+
+ if (windows.get(nextWindowI).isShown()) {
+ activateWindow(windows.get(nextWindowI));
+ break;
+ }
+ }
+ } // synchronized (windows)
+
+ }
+
+ /**
+ * Add a window to my window list and make it active. Note package
+ * private access.
+ *
+ * @param window new window to add
+ */
+ final void addWindowToApplication(final TWindow window) {
+
+ // Do not add menu windows to the window list.
+ if (window instanceof TMenu) {
+ return;
+ }
+
+ // Do not add the desktop to the window list.
+ if (window instanceof TDesktop) {
+ return;
+ }
+
+ synchronized (windows) {
+ if (windows.contains(window)) {
+ throw new IllegalArgumentException("Window " + window +
+ " is already in window list");
+ }
+
+ // Whatever window might be moving/dragging, stop it now.
+ for (TWindow w: windows) {
+ if (w.inMovements()) {
+ w.stopMovements();
+ }
+ }
+
+ // Do not allow a modal window to spawn a non-modal window. If a
+ // modal window is active, then this window will become modal
+ // too.
+ if (modalWindowActive()) {
+ window.flags |= TWindow.MODAL;
+ window.flags |= TWindow.CENTERED;
+ window.hidden = false;
+ }
+ if (window.isShown()) {
+ for (TWindow w: windows) {
+ if (w.isActive()) {
+ w.setActive(false);
+ w.onUnfocus();
+ }
+ w.setZ(w.getZ() + 1);
+ }
+ }
+ windows.add(window);
+ if (window.isShown()) {
+ activeWindow = window;
+ activeWindow.setZ(0);
+ activeWindow.setActive(true);
+ activeWindow.onFocus();
+ }
+
+ if (((window.flags & TWindow.CENTERED) == 0)
+ && ((window.flags & TWindow.ABSOLUTEXY) == 0)
+ && (smartWindowPlacement == true)
+ && (!(window instanceof TDesktop))
+ ) {
+
+ doSmartPlacement(window);
+ }
+ }
+
+ // Desktop cannot be active over any other window.
+ if (desktop != null) {
+ desktop.setActive(false);
+ }
+ }
+
+ /**
+ * Check if there is a system-modal window on top.
+ *
+ * @return true if the active window is modal
+ */
+ private boolean modalWindowActive() {
+ if (windows.size() == 0) {
+ return false;
+ }
+
+ for (TWindow w: windows) {
+ if (w.isModal()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Check if there is a window with overridden menu flag on top.
+ *
+ * @return true if the active window is overriding the menu
+ */
+ private boolean overrideMenuWindowActive() {
+ if (activeWindow != null) {
+ if (activeWindow.hasOverriddenMenu()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Close all open windows.
+ */
+ private void closeAllWindows() {
+ // Don't do anything if we are in the menu
+ if (activeMenu != null) {
+ return;
+ }
+ while (windows.size() > 0) {
+ closeWindow(windows.get(0));
+ }
+ }
+
+ /**
+ * Re-layout the open windows as non-overlapping tiles. This produces
+ * almost the same results as Turbo Pascal 7.0's IDE.
+ */
+ private void tileWindows() {
+ synchronized (windows) {
+ // Don't do anything if we are in the menu
+ if (activeMenu != null) {
+ return;
+ }
+ int z = windows.size();
+ if (z == 0) {
+ return;
+ }
+ int a = 0;
+ int b = 0;
+ a = (int)(Math.sqrt(z));
+ int c = 0;
+ while (c < a) {
+ b = (z - c) / a;
+ if (((a * b) + c) == z) {
+ break;
+ }
+ c++;
+ }
+ assert (a > 0);
+ assert (b > 0);
+ assert (c < a);
+ int newWidth = (getScreen().getWidth() / a);
+ int newHeight1 = ((getScreen().getHeight() - 1) / b);
+ int newHeight2 = ((getScreen().getHeight() - 1) / (b + c));
+
+ List<TWindow> sorted = new ArrayList<TWindow>(windows);
+ Collections.sort(sorted);
+ Collections.reverse(sorted);
+ for (int i = 0; i < sorted.size(); i++) {
+ int logicalX = i / b;
+ int logicalY = i % b;
+ if (i >= ((a - 1) * b)) {
+ logicalX = a - 1;
+ logicalY = i - ((a - 1) * b);
+ }
+
+ TWindow w = sorted.get(i);
+ int oldWidth = w.getWidth();
+ int oldHeight = w.getHeight();
+
+ w.setX(logicalX * newWidth);
+ w.setWidth(newWidth);
+ if (i >= ((a - 1) * b)) {
+ w.setY((logicalY * newHeight2) + 1);
+ w.setHeight(newHeight2);
+ } else {
+ w.setY((logicalY * newHeight1) + 1);
+ w.setHeight(newHeight1);
+ }
+ if ((w.getWidth() != oldWidth)
+ || (w.getHeight() != oldHeight)
+ ) {
+ w.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
+ w.getWidth(), w.getHeight()));
+ }
+ }
+ }
+ }
+
+ /**
+ * Re-layout the open windows as overlapping cascaded windows.
+ */
+ private void cascadeWindows() {
+ synchronized (windows) {
+ // Don't do anything if we are in the menu
+ if (activeMenu != null) {
+ return;
+ }
+ int x = 0;
+ int y = 1;
+ List<TWindow> sorted = new ArrayList<TWindow>(windows);
+ Collections.sort(sorted);
+ Collections.reverse(sorted);
+ for (TWindow window: sorted) {
+ window.setX(x);
+ window.setY(y);
+ x++;
+ y++;
+ if (x > getScreen().getWidth()) {
+ x = 0;
+ }
+ if (y >= getScreen().getHeight()) {
+ y = 1;
+ }
+ }
+ }
+ }
+
+ /**
+ * Place a window to minimize its overlap with other windows.
+ *
+ * @param window the window to place
+ */
+ public final void doSmartPlacement(final TWindow window) {
+ // This is a pretty dumb algorithm, but seems to work. The hardest
+ // part is computing these "overlap" values seeking a minimum average
+ // overlap.
+ int xMin = 0;
+ int yMin = desktopTop;
+ int xMax = getScreen().getWidth() - window.getWidth() + 1;
+ int yMax = desktopBottom - window.getHeight() + 1;
+ if (xMax < xMin) {
+ xMax = xMin;
+ }
+ if (yMax < yMin) {
+ yMax = yMin;
+ }
+
+ if ((xMin == xMax) && (yMin == yMax)) {
+ // No work to do, bail out.
+ return;
+ }
+
+ // Compute the overlap matrix without the new window.
+ int width = getScreen().getWidth();
+ int height = getScreen().getHeight();
+ int overlapMatrix[][] = new int[width][height];
+ for (TWindow w: windows) {
+ if (window == w) {
+ continue;
+ }
+ for (int x = w.getX(); x < w.getX() + w.getWidth(); x++) {
+ if (x < 0) {
+ continue;
+ }
+ if (x >= width) {
+ continue;
+ }
+ for (int y = w.getY(); y < w.getY() + w.getHeight(); y++) {
+ if (y < 0) {
+ continue;
+ }
+ if (y >= height) {
+ continue;
+ }
+ overlapMatrix[x][y]++;
+ }
+ }
+ }
+
+ long oldOverlapTotal = 0;
+ long oldOverlapN = 0;
+ for (int x = 0; x < width; x++) {
+ for (int y = 0; y < height; y++) {
+ oldOverlapTotal += overlapMatrix[x][y];
+ if (overlapMatrix[x][y] > 0) {
+ oldOverlapN++;
+ }
+ }
+ }
+
+
+ double oldOverlapAvg = (double) oldOverlapTotal / (double) oldOverlapN;
+ boolean first = true;
+ int windowX = window.getX();
+ int windowY = window.getY();
+
+ // For each possible (x, y) position for the new window, compute a
+ // new overlap matrix.
+ for (int x = xMin; x < xMax; x++) {
+ for (int y = yMin; y < yMax; y++) {
+
+ // Start with the matrix minus this window.
+ int newMatrix[][] = new int[width][height];
+ for (int mx = 0; mx < width; mx++) {
+ for (int my = 0; my < height; my++) {
+ newMatrix[mx][my] = overlapMatrix[mx][my];
+ }
+ }
+
+ // Add this window's values to the new overlap matrix.
+ long newOverlapTotal = 0;
+ long newOverlapN = 0;
+ // Start by adding each new cell.
+ for (int wx = x; wx < x + window.getWidth(); wx++) {
+ if (wx >= width) {
+ continue;
+ }
+ for (int wy = y; wy < y + window.getHeight(); wy++) {
+ if (wy >= height) {
+ continue;
+ }
+ newMatrix[wx][wy]++;
+ }
+ }
+ // Now figure out the new value for total coverage.
+ for (int mx = 0; mx < width; mx++) {
+ for (int my = 0; my < height; my++) {
+ newOverlapTotal += newMatrix[x][y];
+ if (newMatrix[mx][my] > 0) {
+ newOverlapN++;
+ }
+ }
+ }
+ double newOverlapAvg = (double) newOverlapTotal / (double) newOverlapN;
+
+ if (first) {
+ // First time: just record what we got.
+ oldOverlapAvg = newOverlapAvg;
+ first = false;
+ } else {
+ // All other times: pick a new best (x, y) and save the
+ // overlap value.
+ if (newOverlapAvg < oldOverlapAvg) {
+ windowX = x;
+ windowY = y;
+ oldOverlapAvg = newOverlapAvg;
+ }
+ }
+
+ } // for (int x = xMin; x < xMax; x++)
+
+ } // for (int y = yMin; y < yMax; y++)
+
+ // Finally, set the window's new coordinates.
+ window.setX(windowX);
+ window.setY(windowY);
+ }
+
+ // ------------------------------------------------------------------------
+ // TMenu management -------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Check if a mouse event would hit either the active menu or any open
+ * sub-menus.
+ *
+ * @param mouse mouse event
+ * @return true if the mouse would hit the active menu or an open
+ * sub-menu
+ */
+ private boolean mouseOnMenu(final TMouseEvent mouse) {
+ assert (activeMenu != null);
+ List<TMenu> menus = new ArrayList<TMenu>(subMenus);
+ Collections.reverse(menus);
+ for (TMenu menu: menus) {
+ if (menu.mouseWouldHit(mouse)) {
+ return true;
+ }
+ }
+ return activeMenu.mouseWouldHit(mouse);
+ }
+
+ /**
+ * See if we need to switch window or activate the menu based on
+ * a mouse click.
+ *
+ * @param mouse mouse event
+ */
+ private void checkSwitchFocus(final TMouseEvent mouse) {
+
+ if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN)
+ && (activeMenu != null)
+ && (mouse.getAbsoluteY() != 0)
+ && (!mouseOnMenu(mouse))
+ ) {
+ // They clicked outside the active menu, turn it off
+ activeMenu.setActive(false);
+ activeMenu = null;
+ for (TMenu menu: subMenus) {
+ menu.setActive(false);
+ }
+ subMenus.clear();
+ // Continue checks
+ }
+
+ // See if they hit the menu bar
+ if ((mouse.getType() == TMouseEvent.Type.MOUSE_DOWN)
+ && (mouse.isMouse1())
+ && (!modalWindowActive())
+ && (!overrideMenuWindowActive())
+ && (mouse.getAbsoluteY() == 0)
+ && (hideMenuBar == false)
+ ) {
+
+ for (TMenu menu: subMenus) {
+ menu.setActive(false);
+ }
+ subMenus.clear();
+
+ // They selected the menu, go activate it
+ for (TMenu menu: menus) {
+ if ((mouse.getAbsoluteX() >= menu.getTitleX())
+ && (mouse.getAbsoluteX() < menu.getTitleX()
+ + StringUtils.width(menu.getTitle()) + 2)
+ ) {
+ menu.setActive(true);
+ activeMenu = menu;
+ } else {
+ menu.setActive(false);
+ }
+ }
+ return;
+ }
+
+ // See if they hit the menu bar
+ if ((mouse.getType() == TMouseEvent.Type.MOUSE_MOTION)
+ && (mouse.isMouse1())
+ && (activeMenu != null)
+ && (mouse.getAbsoluteY() == 0)
+ && (hideMenuBar == false)
+ ) {
+
+ TMenu oldMenu = activeMenu;
+ for (TMenu menu: subMenus) {
+ menu.setActive(false);
+ }
+ subMenus.clear();
+
+ // See if we should switch menus
+ for (TMenu menu: menus) {
+ if ((mouse.getAbsoluteX() >= menu.getTitleX())
+ && (mouse.getAbsoluteX() < menu.getTitleX()
+ + StringUtils.width(menu.getTitle()) + 2)
+ ) {
+ menu.setActive(true);
+ activeMenu = menu;
+ }
+ }
+ if (oldMenu != activeMenu) {
+ // They switched menus
+ oldMenu.setActive(false);
+ }
+ return;
+ }
+
+ // If a menu is still active, don't switch windows
+ if (activeMenu != null) {
+ return;
+ }
+
+ // Only switch if there are multiple windows
+ if (windows.size() < 2) {
+ return;
+ }
+
+ if (((focusFollowsMouse == true)
+ && (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION))
+ || (mouse.getType() == TMouseEvent.Type.MOUSE_DOWN)
+ ) {
+ synchronized (windows) {
+ Collections.sort(windows);
+ if (windows.get(0).isModal()) {
+ // Modal windows don't switch
+ return;
+ }
+
+ for (TWindow window: windows) {
+ assert (!window.isModal());
+
+ if (window.isHidden()) {
+ assert (!window.isActive());
+ continue;
+ }
+
+ 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();
+ return;
+ }
+ }
+ }
+
+ // Clicked on the background, nothing to do
+ return;
+ }
+
+ // Nothing to do: this isn't a mouse up, or focus isn't following
+ // mouse.
+ return;
+ }
+
+ /**
+ * Turn off the menu.
+ */
+ public final void closeMenu() {
+ if (activeMenu != null) {
+ activeMenu.setActive(false);
+ activeMenu = null;
+ for (TMenu menu: subMenus) {
+ menu.setActive(false);
+ }
+ subMenus.clear();
+ }
+ }
+
+ /**
+ * Get a (shallow) copy of the menu list.
+ *
+ * @return a copy of the menu list
+ */
+ public final List<TMenu> getAllMenus() {
+ return new ArrayList<TMenu>(menus);
+ }
+
+ /**
+ * Add a top-level menu to the list.
+ *
+ * @param menu the menu to add
+ * @throws IllegalArgumentException if the menu is already used in
+ * another TApplication
+ */
+ public final void addMenu(final TMenu menu) {
+ if ((menu.getApplication() != null)
+ && (menu.getApplication() != this)
+ ) {
+ throw new IllegalArgumentException("Menu " + menu + " is already " +
+ "part of application " + menu.getApplication());
+ }
+ closeMenu();
+ menus.add(menu);
+ recomputeMenuX();
+ }
+
+ /**
+ * Remove a top-level menu from the list.
+ *
+ * @param menu the menu to remove
+ * @throws IllegalArgumentException if the menu is already used in
+ * another TApplication
+ */
+ public final void removeMenu(final TMenu menu) {
+ if ((menu.getApplication() != null)
+ && (menu.getApplication() != this)
+ ) {
+ throw new IllegalArgumentException("Menu " + menu + " is already " +
+ "part of application " + menu.getApplication());
+ }
+ closeMenu();
+ menus.remove(menu);
+ recomputeMenuX();
+ }
+
+ /**
+ * Turn off a sub-menu.
+ */
+ public final void closeSubMenu() {
+ assert (activeMenu != null);
+ TMenu item = subMenus.get(subMenus.size() - 1);
+ assert (item != null);
+ item.setActive(false);
+ subMenus.remove(subMenus.size() - 1);
+ }
+
+ /**
+ * Switch to the next menu.
+ *
+ * @param forward if true, then switch to the next menu in the list,
+ * otherwise switch to the previous menu in the list
+ */
+ public final void switchMenu(final boolean forward) {
+ assert (activeMenu != null);
+ assert (hideMenuBar == false);
+
+ for (TMenu menu: subMenus) {
+ menu.setActive(false);
+ }
+ subMenus.clear();
+
+ for (int i = 0; i < menus.size(); i++) {
+ if (activeMenu == menus.get(i)) {
+ if (forward) {
+ if (i < menus.size() - 1) {
+ i++;
+ } else {
+ i = 0;
+ }
+ } else {
+ if (i > 0) {
+ i--;
+ } else {
+ i = menus.size() - 1;
+ }
+ }
+ activeMenu.setActive(false);
+ activeMenu = menus.get(i);
+ activeMenu.setActive(true);
+ return;
+ }
+ }
+ }
+
+ /**
+ * Add a menu item to the global list. If it has a keyboard accelerator,
+ * that will be added the global hash.
+ *
+ * @param item the menu item
+ */
+ public final void addMenuItem(final TMenuItem item) {
+ menuItems.add(item);
+
+ TKeypress key = item.getKey();
+ if (key != null) {
+ synchronized (accelerators) {
+ assert (accelerators.get(key) == null);
+ accelerators.put(key.toLowerCase(), item);
+ }
+ }
+ }
+
+ /**
+ * Disable one menu item.
+ *
+ * @param id the menu item ID
+ */
+ public final void disableMenuItem(final int id) {
+ for (TMenuItem item: menuItems) {
+ if (item.getId() == id) {
+ item.setEnabled(false);
+ }
+ }
+ }
+
+ /**
+ * Disable the range of menu items with ID's between lower and upper,
+ * inclusive.
+ *
+ * @param lower the lowest menu item ID
+ * @param upper the highest menu item ID
+ */
+ public final void disableMenuItems(final int lower, final int upper) {
+ for (TMenuItem item: menuItems) {
+ if ((item.getId() >= lower) && (item.getId() <= upper)) {
+ item.setEnabled(false);
+ item.getParent().activate(0);
+ }
+ }
+ }
+
+ /**
+ * Enable one menu item.
+ *
+ * @param id the menu item ID
+ */
+ public final void enableMenuItem(final int id) {
+ for (TMenuItem item: menuItems) {
+ if (item.getId() == id) {
+ item.setEnabled(true);
+ item.getParent().activate(0);
+ }
+ }
+ }
+
+ /**
+ * Enable the range of menu items with ID's between lower and upper,
+ * inclusive.
+ *
+ * @param lower the lowest menu item ID
+ * @param upper the highest menu item ID
+ */
+ public final void enableMenuItems(final int lower, final int upper) {
+ for (TMenuItem item: menuItems) {
+ if ((item.getId() >= lower) && (item.getId() <= upper)) {
+ item.setEnabled(true);
+ item.getParent().activate(0);
+ }
+ }
+ }
+
+ /**
+ * Get the menu item associated with this ID.
+ *
+ * @param id the menu item ID
+ * @return the menu item, or null if not found
+ */
+ public final TMenuItem getMenuItem(final int id) {
+ for (TMenuItem item: menuItems) {
+ if (item.getId() == id) {
+ return item;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Recompute menu x positions based on their title length.
+ */
+ public final void recomputeMenuX() {
+ int x = 0;
+ for (TMenu menu: menus) {
+ menu.setX(x);
+ menu.setTitleX(x);
+ x += StringUtils.width(menu.getTitle()) + 2;
+
+ // Don't let the menu window exceed the screen width
+ int rightEdge = menu.getX() + menu.getWidth();
+ if (rightEdge > getScreen().getWidth()) {
+ menu.setX(getScreen().getWidth() - menu.getWidth());
+ }
+ }
+ }
+
+ /**
+ * Post an event to process.
+ *
+ * @param event new event to add to the queue
+ */
+ public final void postEvent(final TInputEvent event) {
+ synchronized (this) {
+ synchronized (fillEventQueue) {
+ fillEventQueue.add(event);
+ }
+ if (debugThreads) {
+ System.err.println(System.currentTimeMillis() + " " +
+ Thread.currentThread() + " postEvent() wake up main");
+ }
+ this.notify();
+ }
+ }
+
+ /**
+ * Post an event to process and turn off the menu.
+ *
+ * @param event new event to add to the queue
+ */
+ public final void postMenuEvent(final TInputEvent event) {
+ synchronized (this) {
+ synchronized (fillEventQueue) {
+ fillEventQueue.add(event);
+ }
+ if (debugThreads) {
+ System.err.println(System.currentTimeMillis() + " " +
+ Thread.currentThread() + " postMenuEvent() wake up main");
+ }
+ closeMenu();
+ this.notify();
+ }
+ }
+
+ /**
+ * Add a sub-menu to the list of open sub-menus.
+ *
+ * @param menu sub-menu
+ */
+ public final void addSubMenu(final TMenu menu) {
+ subMenus.add(menu);
+ }
+
+ /**
+ * Convenience function to add a top-level menu.
+ *
+ * @param title menu title
+ * @return the new menu
+ */
+ public final TMenu addMenu(final String title) {
+ int x = 0;
+ int y = 0;
+ TMenu menu = new TMenu(this, x, y, title);
+ menus.add(menu);
+ recomputeMenuX();
+ return menu;
+ }
+
+ /**
+ * Convenience function to add a default tools (hamburger) menu.
+ *
+ * @return the new menu
+ */
+ public final TMenu addToolMenu() {
+ TMenu toolMenu = addMenu(i18n.getString("toolMenuTitle"));
+ toolMenu.addDefaultItem(TMenu.MID_REPAINT);
+ toolMenu.addDefaultItem(TMenu.MID_VIEW_IMAGE);
+ toolMenu.addDefaultItem(TMenu.MID_SCREEN_OPTIONS);
+ TStatusBar toolStatusBar = toolMenu.newStatusBar(i18n.
+ getString("toolMenuStatus"));
+ toolStatusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help"));
+ return toolMenu;
+ }
+
+ /**
+ * Convenience function to add a default "File" menu.
+ *
+ * @return the new menu
+ */
+ public final TMenu addFileMenu() {
+ TMenu fileMenu = addMenu(i18n.getString("fileMenuTitle"));
+ fileMenu.addDefaultItem(TMenu.MID_SHELL);
+ fileMenu.addSeparator();
+ fileMenu.addDefaultItem(TMenu.MID_EXIT);
+ TStatusBar statusBar = fileMenu.newStatusBar(i18n.
+ getString("fileMenuStatus"));
+ statusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help"));
+ return fileMenu;
+ }
+
+ /**
+ * Convenience function to add a default "Edit" menu.
+ *
+ * @return the new menu
+ */
+ 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);
+ TStatusBar statusBar = editMenu.newStatusBar(i18n.
+ getString("editMenuStatus"));
+ statusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help"));
+ return editMenu;
+ }
+
+ /**
+ * Convenience function to add a default "Window" menu.
+ *
+ * @return the new menu
+ */
+ public final TMenu addWindowMenu() {
+ TMenu windowMenu = addMenu(i18n.getString("windowMenuTitle"));
+ windowMenu.addDefaultItem(TMenu.MID_TILE);
+ windowMenu.addDefaultItem(TMenu.MID_CASCADE);
+ windowMenu.addDefaultItem(TMenu.MID_CLOSE_ALL);
+ windowMenu.addSeparator();
+ windowMenu.addDefaultItem(TMenu.MID_WINDOW_MOVE);
+ windowMenu.addDefaultItem(TMenu.MID_WINDOW_ZOOM);
+ windowMenu.addDefaultItem(TMenu.MID_WINDOW_NEXT);
+ windowMenu.addDefaultItem(TMenu.MID_WINDOW_PREVIOUS);
+ windowMenu.addDefaultItem(TMenu.MID_WINDOW_CLOSE);
+ TStatusBar statusBar = windowMenu.newStatusBar(i18n.
+ getString("windowMenuStatus"));
+ statusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help"));
+ return windowMenu;
+ }
+
+ /**
+ * Convenience function to add a default "Help" menu.
+ *
+ * @return the new menu
+ */
+ public final TMenu addHelpMenu() {
+ TMenu helpMenu = addMenu(i18n.getString("helpMenuTitle"));
+ helpMenu.addDefaultItem(TMenu.MID_HELP_CONTENTS);
+ helpMenu.addDefaultItem(TMenu.MID_HELP_INDEX);
+ helpMenu.addDefaultItem(TMenu.MID_HELP_SEARCH);
+ helpMenu.addDefaultItem(TMenu.MID_HELP_PREVIOUS);
+ helpMenu.addDefaultItem(TMenu.MID_HELP_HELP);
+ helpMenu.addDefaultItem(TMenu.MID_HELP_ACTIVE_FILE);
+ helpMenu.addSeparator();
+ helpMenu.addDefaultItem(TMenu.MID_ABOUT);
+ TStatusBar statusBar = helpMenu.newStatusBar(i18n.
+ getString("helpMenuStatus"));
+ statusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help"));
+ return helpMenu;
+ }
+
+ /**
+ * Convenience function to add a default "Table" menu.
+ *
+ * @return the new menu
+ */
+ public final TMenu addTableMenu() {
+ TMenu tableMenu = addMenu(i18n.getString("tableMenuTitle"));
+ tableMenu.addDefaultItem(TMenu.MID_TABLE_RENAME_COLUMN, false);
+ tableMenu.addDefaultItem(TMenu.MID_TABLE_RENAME_ROW, false);
+ tableMenu.addSeparator();
+
+ TSubMenu viewMenu = tableMenu.addSubMenu(i18n.
+ getString("tableSubMenuView"));
+ viewMenu.addDefaultItem(TMenu.MID_TABLE_VIEW_ROW_LABELS, false);
+ viewMenu.addDefaultItem(TMenu.MID_TABLE_VIEW_COLUMN_LABELS, false);
+ viewMenu.addDefaultItem(TMenu.MID_TABLE_VIEW_HIGHLIGHT_ROW, false);
+ viewMenu.addDefaultItem(TMenu.MID_TABLE_VIEW_HIGHLIGHT_COLUMN, false);
+
+ TSubMenu borderMenu = tableMenu.addSubMenu(i18n.
+ getString("tableSubMenuBorders"));
+ borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_NONE, false);
+ borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_ALL, false);
+ borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_CELL_NONE, false);
+ borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_CELL_ALL, false);
+ borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_RIGHT, false);
+ borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_LEFT, false);
+ borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_TOP, false);
+ borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_BOTTOM, false);
+ borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_DOUBLE_BOTTOM, false);
+ borderMenu.addDefaultItem(TMenu.MID_TABLE_BORDER_THICK_BOTTOM, false);
+ TSubMenu deleteMenu = tableMenu.addSubMenu(i18n.
+ getString("tableSubMenuDelete"));
+ deleteMenu.addDefaultItem(TMenu.MID_TABLE_DELETE_LEFT, false);
+ deleteMenu.addDefaultItem(TMenu.MID_TABLE_DELETE_UP, false);
+ deleteMenu.addDefaultItem(TMenu.MID_TABLE_DELETE_ROW, false);
+ deleteMenu.addDefaultItem(TMenu.MID_TABLE_DELETE_COLUMN, false);
+ TSubMenu insertMenu = tableMenu.addSubMenu(i18n.
+ getString("tableSubMenuInsert"));
+ insertMenu.addDefaultItem(TMenu.MID_TABLE_INSERT_LEFT, false);
+ insertMenu.addDefaultItem(TMenu.MID_TABLE_INSERT_RIGHT, false);
+ insertMenu.addDefaultItem(TMenu.MID_TABLE_INSERT_ABOVE, false);
+ insertMenu.addDefaultItem(TMenu.MID_TABLE_INSERT_BELOW, false);
+ TSubMenu columnMenu = tableMenu.addSubMenu(i18n.
+ getString("tableSubMenuColumn"));
+ columnMenu.addDefaultItem(TMenu.MID_TABLE_COLUMN_NARROW, false);
+ columnMenu.addDefaultItem(TMenu.MID_TABLE_COLUMN_WIDEN, false);
+ TSubMenu fileMenu = tableMenu.addSubMenu(i18n.
+ getString("tableSubMenuFile"));
+ fileMenu.addDefaultItem(TMenu.MID_TABLE_FILE_OPEN_CSV, false);
+ fileMenu.addDefaultItem(TMenu.MID_TABLE_FILE_SAVE_CSV, false);
+ fileMenu.addDefaultItem(TMenu.MID_TABLE_FILE_SAVE_TEXT, false);
+
+ TStatusBar statusBar = tableMenu.newStatusBar(i18n.
+ getString("tableMenuStatus"));
+ statusBar.addShortcutKeypress(kbF1, cmHelp, i18n.getString("Help"));
+ return tableMenu;
+ }
+
+ // ------------------------------------------------------------------------
+ // TTimer management ------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get the amount of time I can sleep before missing a Timer tick.
+ *
+ * @param timeout = initial (maximum) timeout in millis
+ * @return number of milliseconds between now and the next timer event
+ */
+ private long getSleepTime(final long timeout) {
+ Date now = new Date();
+ long nowTime = now.getTime();
+ long sleepTime = timeout;
+
+ synchronized (timers) {
+ for (TTimer timer: timers) {
+ long nextTickTime = timer.getNextTick().getTime();
+ if (nextTickTime < nowTime) {
+ return 0;
+ }
+
+ long timeDifference = nextTickTime - nowTime;
+ if (timeDifference < sleepTime) {
+ sleepTime = timeDifference;
+ }
+ }
+ }
+
+ assert (sleepTime >= 0);
+ assert (sleepTime <= timeout);
+ return sleepTime;
+ }
+
+ /**
+ * Convenience function to add a timer.
+ *
+ * @param duration number of milliseconds to wait between ticks
+ * @param recurring if true, re-schedule this timer after every tick
+ * @param action function to call when button is pressed
+ * @return the timer
+ */
+ public final TTimer addTimer(final long duration, final boolean recurring,
+ final TAction action) {
+
+ TTimer timer = new TTimer(duration, recurring, action);
+ synchronized (timers) {
+ timers.add(timer);
+ }
+ return timer;
+ }
+
+ /**
+ * Convenience function to remove a timer.
+ *
+ * @param timer timer to remove
+ */
+ public final void removeTimer(final TTimer timer) {
+ synchronized (timers) {
+ timers.remove(timer);
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // Other TWindow constructors ---------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Convenience function to spawn a message box.
+ *
+ * @param title window title, will be centered along the top border
+ * @param caption message to display. Use embedded newlines to get a
+ * multi-line box.
+ * @return the new message box
+ */
+ public final TMessageBox messageBox(final String title,
+ final String caption) {
+
+ return new TMessageBox(this, title, caption, TMessageBox.Type.OK);
+ }
+
+ /**
+ * Convenience function to spawn a message box.
+ *
+ * @param title window title, will be centered along the top border
+ * @param caption message to display. Use embedded newlines to get a
+ * multi-line box.
+ * @param type one of the TMessageBox.Type constants. Default is
+ * Type.OK.
+ * @return the new message box
+ */
+ public final TMessageBox messageBox(final String title,
+ final String caption, final TMessageBox.Type type) {
+
+ return new TMessageBox(this, title, caption, type);
+ }
+
+ /**
+ * Convenience function to spawn an input box.
+ *
+ * @param title window title, will be centered along the top border
+ * @param caption message to display. Use embedded newlines to get a
+ * multi-line box.
+ * @return the new input box
+ */
+ public final TInputBox inputBox(final String title, final String caption) {
+
+ return new TInputBox(this, title, caption);
+ }
+
+ /**
+ * Convenience function to spawn an input box.
+ *
+ * @param title window title, will be centered along the top border
+ * @param caption message to display. Use embedded newlines to get a
+ * multi-line box.
+ * @param text initial text to seed the field with
+ * @return the new input box
+ */
+ public final TInputBox inputBox(final String title, final String caption,
+ final String text) {
+
+ return new TInputBox(this, title, caption, text);
+ }
+
+ /**
+ * Convenience function to spawn an input box.
+ *
+ * @param title window title, will be centered along the top border
+ * @param caption message to display. Use embedded newlines to get a
+ * multi-line box.
+ * @param text initial text to seed the field with
+ * @param type one of the Type constants. Default is Type.OK.
+ * @return the new input box
+ */
+ public final TInputBox inputBox(final String title, final String caption,
+ final String text, final TInputBox.Type type) {
+
+ return new TInputBox(this, title, caption, text, type);
+ }
+
+ /**
+ * Convenience function to open a terminal window.
+ *
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @return the terminal new window
+ */
+ public final TTerminalWindow openTerminal(final int x, final int y) {
+ return openTerminal(x, y, TWindow.RESIZABLE);
+ }
+
+ /**
+ * Convenience function to open a terminal window.
+ *
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param closeOnExit if true, close the window when the command exits
+ * @return the terminal new window
+ */
+ public final TTerminalWindow openTerminal(final int x, final int y,
+ final boolean closeOnExit) {
+
+ return openTerminal(x, y, TWindow.RESIZABLE, closeOnExit);
+ }
+
+ /**
+ * Convenience function to open a terminal window.
+ *
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param flags mask of CENTERED, MODAL, or RESIZABLE
+ * @return the terminal new window
+ */
+ public final TTerminalWindow openTerminal(final int x, final int y,
+ final int flags) {
+
+ return new TTerminalWindow(this, x, y, flags);
+ }
+
+ /**
+ * Convenience function to open a terminal window.
+ *
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param flags mask of CENTERED, MODAL, or RESIZABLE
+ * @param closeOnExit if true, close the window when the command exits
+ * @return the terminal new window
+ */
+ public final TTerminalWindow openTerminal(final int x, final int y,
+ final int flags, final boolean closeOnExit) {
+
+ return new TTerminalWindow(this, x, y, flags, closeOnExit);
+ }
+
+ /**
+ * Convenience function to open a terminal window and execute a custom
+ * command line inside it.
+ *
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param commandLine the command line to execute
+ * @return the terminal new window
+ */
+ public final TTerminalWindow openTerminal(final int x, final int y,
+ final String commandLine) {
+
+ return openTerminal(x, y, TWindow.RESIZABLE, commandLine);
+ }
+
+ /**
+ * Convenience function to open a terminal window and execute a custom
+ * command line inside it.
+ *
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param commandLine the command line to execute
+ * @param closeOnExit if true, close the window when the command exits
+ * @return the terminal new window
+ */
+ public final TTerminalWindow openTerminal(final int x, final int y,
+ final String commandLine, final boolean closeOnExit) {
+
+ return openTerminal(x, y, TWindow.RESIZABLE, commandLine, closeOnExit);
+ }
+
+ /**
+ * Convenience function to open a terminal window and execute a custom
+ * command line inside it.
+ *
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param flags mask of CENTERED, MODAL, or RESIZABLE
+ * @param command the command line to execute
+ * @return the terminal new window
+ */
+ public final TTerminalWindow openTerminal(final int x, final int y,
+ final int flags, final String [] command) {
+
+ return new TTerminalWindow(this, x, y, flags, command);
+ }
+
+ /**
+ * Convenience function to open a terminal window and execute a custom
+ * command line inside it.
+ *
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param flags mask of CENTERED, MODAL, or RESIZABLE
+ * @param command the command line to execute
+ * @param closeOnExit if true, close the window when the command exits
+ * @return the terminal new window
+ */
+ public final TTerminalWindow openTerminal(final int x, final int y,
+ final int flags, final String [] command, final boolean closeOnExit) {
+
+ return new TTerminalWindow(this, x, y, flags, command, closeOnExit);
+ }
+
+ /**
+ * Convenience function to open a terminal window and execute a custom
+ * command line inside it.
+ *
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param flags mask of CENTERED, MODAL, or RESIZABLE
+ * @param commandLine the command line to execute
+ * @return the terminal new window
+ */
+ public final TTerminalWindow openTerminal(final int x, final int y,
+ final int flags, final String commandLine) {
+
+ return new TTerminalWindow(this, x, y, flags, commandLine.split("\\s+"));
+ }
+
+ /**
+ * Convenience function to open a terminal window and execute a custom
+ * command line inside it.
+ *
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param flags mask of CENTERED, MODAL, or RESIZABLE
+ * @param commandLine the command line to execute
+ * @param closeOnExit if true, close the window when the command exits
+ * @return the terminal new window
+ */
+ public final TTerminalWindow openTerminal(final int x, final int y,
+ final int flags, final String commandLine, final boolean closeOnExit) {
+
+ return new TTerminalWindow(this, x, y, flags, commandLine.split("\\s+"),
+ closeOnExit);
+ }
+
+ /**
+ * Convenience function to spawn an file open box.
+ *
+ * @param path path of selected file
+ * @return the result of the new file open box
+ * @throws IOException if java.io operation throws
+ */
+ public final String fileOpenBox(final String path) throws IOException {
+
+ TFileOpenBox box = new TFileOpenBox(this, path, TFileOpenBox.Type.OPEN);
+ return box.getFilename();
+ }
+
+ /**
+ * Convenience function to spawn an file open box.
+ *
+ * @param path path of selected file
+ * @param type one of the Type constants
+ * @return the result of the new file open box
+ * @throws IOException if java.io operation throws
+ */
+ public final String fileOpenBox(final String path,
+ final TFileOpenBox.Type type) throws IOException {
+
+ TFileOpenBox box = new TFileOpenBox(this, path, type);
+ return box.getFilename();
+ }
+
+ /**
+ * Convenience function to spawn a file open box.
+ *
+ * @param path path of selected file
+ * @param type one of the Type constants
+ * @param filter a string that files must match to be displayed
+ * @return the result of the new file open box
+ * @throws IOException of a java.io operation throws
+ */
+ public final String fileOpenBox(final String path,
+ final TFileOpenBox.Type type, final String filter) throws IOException {
+
+ ArrayList<String> filters = new ArrayList<String>();
+ filters.add(filter);
+
+ TFileOpenBox box = new TFileOpenBox(this, path, type, filters);
+ return box.getFilename();
+ }
+
+ /**
+ * Convenience function to spawn a file open box.
+ *
+ * @param path path of selected file
+ * @param type one of the Type constants
+ * @param filters a list of strings that files must match to be displayed
+ * @return the result of the new file open box
+ * @throws IOException of a java.io operation throws
+ */
+ public final String fileOpenBox(final String path,
+ final TFileOpenBox.Type type,
+ final List<String> filters) throws IOException {
+
+ TFileOpenBox box = new TFileOpenBox(this, path, type, filters);
+ return box.getFilename();
+ }
+
+ /**
+ * Convenience function to create a new window and make it active.
+ * Window will be located at (0, 0).
+ *
+ * @param title window title, will be centered along the top border
+ * @param width width of window
+ * @param height height of window
+ * @return the new window
+ */
+ public final TWindow addWindow(final String title, final int width,
+ final int height) {
+
+ TWindow window = new TWindow(this, title, 0, 0, width, height);
+ return window;
+ }
+
+ /**
+ * Convenience function to create a new window and make it active.
+ * Window will be located at (0, 0).
+ *
+ * @param title window title, will be centered along the top border
+ * @param width width of window
+ * @param height height of window
+ * @param flags bitmask of RESIZABLE, CENTERED, or MODAL
+ * @return the new window
+ */
+ public final TWindow addWindow(final String title,
+ final int width, final int height, final int flags) {
+
+ TWindow window = new TWindow(this, title, 0, 0, width, height, flags);
+ return window;
+ }
+
+ /**
+ * Convenience function to create a new window and make it active.
+ *
+ * @param title window title, will be centered along the top border
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of window
+ * @param height height of window
+ * @return the new window
+ */
+ public final TWindow addWindow(final String title,
+ final int x, final int y, final int width, final int height) {
+
+ TWindow window = new TWindow(this, title, x, y, width, height);
+ return window;
+ }
+
+ /**
+ * Convenience function to create a new window and make it active.
+ *
+ * @param title window title, will be centered along the top border
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of window
+ * @param height height of window
+ * @param flags mask of RESIZABLE, CENTERED, or MODAL
+ * @return the new window
+ */
+ public final TWindow addWindow(final String title,
+ final int x, final int y, final int width, final int height,
+ final int flags) {
+
+ TWindow window = new TWindow(this, title, x, y, width, height, flags);
+ return window;
+ }
+
+ }
--- /dev/null
+ Help=Help
+
+ toolMenuTitle=&\u2261
+ toolMenuStatus=Additional tools
+ fileMenuTitle=&File
+ fileMenuStatus=File-management commands (Open, Save, Print, etc.)
+ editMenuTitle=&Edit
+ editMenuStatus=Editor operations, undo, and Clipboard access
+ windowMenuTitle=&Window
+ windowMenuStatus=Open, arrange, and list windows
+ helpMenuTitle=&Help
+ helpMenuStatus=Access online help
+
+ tableMenuTitle=&Table
+ tableSubMenuView=&View
+ tableSubMenuBorders=&Borders
+ tableSubMenuDelete=&Delete
+ tableSubMenuInsert=&Insert
+ tableSubMenuColumn=&Column
+ tableSubMenuFile=&File
+ tableMenuStatus=Table manipulation commands
+
+ exitDialogTitle=Confirmation
+ exitDialogText=Exit application?
+
+ aboutDialogTitle=About
+ aboutDialogText=Jexer Version {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;
+
+ import jexer.bits.CellAttributes;
+ import jexer.bits.Color;
+ import jexer.bits.GraphicsChars;
+ import jexer.bits.MnemonicString;
+ import jexer.bits.StringUtils;
+ import jexer.event.TKeypressEvent;
+ import jexer.event.TMouseEvent;
+ import static jexer.TKeypress.kbEnter;
+ import static jexer.TKeypress.kbSpace;
+
+ /**
+ * TButton implements a simple button. To make the button do something, pass
+ * a TAction class to its constructor.
+ *
+ * @see TAction#DO()
+ */
+ public class TButton extends TWidget {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The shortcut and button text.
+ */
+ private MnemonicString mnemonic;
+
+ /**
+ * Remember mouse state.
+ */
+ private TMouseEvent mouse;
+
+ /**
+ * True when the button is being pressed and held down.
+ */
+ private boolean inButtonPress = false;
+
+ /**
+ * The action to perform when the button is clicked.
+ */
+ private TAction action;
+
+ /**
+ * The background color used for the button "shadow", or null for "no
+ * shadow".
+ */
+ private CellAttributes shadowColor;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Private constructor.
+ *
+ * @param parent parent widget
+ * @param text label on the button
+ * @param x column relative to parent
+ * @param y row relative to parent
+ */
+ private TButton(final TWidget parent, final String text,
+ final int x, final int y) {
+
+ // Set parent and window
+ super(parent);
+
+ mnemonic = new MnemonicString(text);
+
+ setX(x);
+ setY(y);
+ super.setHeight(2);
+ super.setWidth(StringUtils.width(mnemonic.getRawLabel()) + 3);
+
+ shadowColor = new CellAttributes();
+ shadowColor.setTo(getWindow().getBackground());
+ shadowColor.setForeColor(Color.BLACK);
+ shadowColor.setBold(false);
+
+ // Since we set dimensions after TWidget's constructor, we need to
+ // update the layout manager.
+ if (getParent().getLayoutManager() != null) {
+ getParent().getLayoutManager().remove(this);
+ getParent().getLayoutManager().add(this);
+ }
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param text label on the button
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param action to call when button is pressed
+ */
+ public TButton(final TWidget parent, final String text,
+ final int x, final int y, final TAction action) {
+
+ this(parent, text, x, y);
+ this.action = action;
+ }
+
+ // ------------------------------------------------------------------------
+ // Event handlers ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Returns true if the mouse is currently on the button.
+ *
+ * @return if true the mouse is currently on the button
+ */
+ private boolean mouseOnButton() {
+ int rightEdge = getWidth() - 1;
+ if (inButtonPress) {
+ rightEdge++;
+ }
+ if ((mouse != null)
+ && (mouse.getY() == 0)
+ && (mouse.getX() >= 0)
+ && (mouse.getX() < rightEdge)
+ ) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Handle mouse button presses.
+ *
+ * @param mouse mouse button event
+ */
+ @Override
+ public void onMouseDown(final TMouseEvent mouse) {
+ this.mouse = mouse;
+
+ if ((mouseOnButton()) && (mouse.isMouse1())) {
+ // Begin button press
+ inButtonPress = true;
+ }
+ }
+
+ /**
+ * Handle mouse button releases.
+ *
+ * @param mouse mouse button release event
+ */
+ @Override
+ public void onMouseUp(final TMouseEvent mouse) {
+ this.mouse = mouse;
+
+ if (inButtonPress && mouse.isMouse1()) {
+ // Dispatch the event
+ dispatch();
+ }
+
+ }
+
+ /**
+ * Handle mouse movements.
+ *
+ * @param mouse mouse motion event
+ */
+ @Override
+ public void onMouseMotion(final TMouseEvent mouse) {
+ this.mouse = mouse;
+
+ if (!mouseOnButton()) {
+ inButtonPress = false;
+ }
+ }
+
+ /**
+ * Handle keystrokes.
+ *
+ * @param keypress keystroke event
+ */
+ @Override
+ public void onKeypress(final TKeypressEvent keypress) {
+ if (keypress.equals(kbEnter)
+ || keypress.equals(kbSpace)
+ ) {
+ // Dispatch
+ dispatch();
+ return;
+ }
+
+ // Pass to parent for the things we don't care about.
+ super.onKeypress(keypress);
+ }
+
+ // ------------------------------------------------------------------------
+ // TWidget ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Override TWidget's width: we can only set width at construction time.
+ *
+ * @param width new widget width (ignored)
+ */
+ @Override
+ public void setWidth(final int width) {
+ // Do nothing
+ }
+
+ /**
+ * Override TWidget's height: we can only set height at construction
+ * time.
+ *
+ * @param height new widget height (ignored)
+ */
+ @Override
+ public void setHeight(final int height) {
+ // Do nothing
+ }
+
+ /**
+ * Draw a button with a shadow.
+ */
+ @Override
+ public void draw() {
+ CellAttributes buttonColor;
+ CellAttributes menuMnemonicColor;
+
+ if (!isEnabled()) {
+ buttonColor = getTheme().getColor("tbutton.disabled");
+ menuMnemonicColor = getTheme().getColor("tbutton.disabled");
+ } else if (isAbsoluteActive()) {
+ buttonColor = getTheme().getColor("tbutton.active");
+ menuMnemonicColor = getTheme().getColor("tbutton.mnemonic.highlighted");
+ } else {
+ buttonColor = getTheme().getColor("tbutton.inactive");
+ menuMnemonicColor = getTheme().getColor("tbutton.mnemonic");
+ }
+
+ if (inButtonPress) {
+ putCharXY(1, 0, ' ', buttonColor);
+ putStringXY(2, 0, mnemonic.getRawLabel(), buttonColor);
+ putCharXY(getWidth() - 1, 0, ' ', buttonColor);
+ } else {
+ putCharXY(0, 0, ' ', buttonColor);
+ putStringXY(1, 0, mnemonic.getRawLabel(), buttonColor);
+ putCharXY(getWidth() - 2, 0, ' ', buttonColor);
+
+ if (shadowColor != null) {
+ putCharXY(getWidth() - 1, 0,
+ GraphicsChars.CP437[0xDC], shadowColor);
+ hLineXY(1, 1, getWidth() - 1,
+ GraphicsChars.CP437[0xDF], shadowColor);
+ }
+ }
+ if (mnemonic.getScreenShortcutIdx() >= 0) {
+ if (inButtonPress) {
+ putCharXY(2 + mnemonic.getScreenShortcutIdx(), 0,
+ mnemonic.getShortcut(), menuMnemonicColor);
+ } else {
+ putCharXY(1 + mnemonic.getScreenShortcutIdx(), 0,
+ mnemonic.getShortcut(), menuMnemonicColor);
+ }
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // TButton ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get the mnemonic string for this button.
+ *
+ * @return mnemonic string
+ */
+ public MnemonicString getMnemonic() {
+ return mnemonic;
+ }
+
+ /**
+ * Act as though the button was pressed. This is useful for other UI
+ * elements to get the same action as if the user clicked the button.
+ */
+ public void dispatch() {
+ if (action != null) {
+ action.DO(this);
+ inButtonPress = false;
+ }
+ }
+
+ /**
+ * Set the background color used for the button "shadow". If null, no
+ * shadow will be drawn.
+ *
+ * @param color the new background color, or null for no shadow
+ */
+ public void setShadowColor(final CellAttributes color) {
+ if (color != null) {
+ shadowColor = new CellAttributes();
+ shadowColor.setTo(color);
+ shadowColor.setForeColor(Color.BLACK);
+ shadowColor.setBold(false);
+ } else {
+ shadowColor = 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;
+
+ import java.util.Calendar;
+ import java.util.GregorianCalendar;
+
+ import jexer.bits.CellAttributes;
+ import jexer.bits.GraphicsChars;
+ import jexer.bits.StringUtils;
+ import jexer.event.TKeypressEvent;
+ import jexer.event.TMouseEvent;
+ import static jexer.TKeypress.*;
+
+ /**
+ * TCalendar is a date picker widget.
+ */
+ public class TCalendar extends TWidget {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The calendar being displayed.
+ */
+ private GregorianCalendar displayCalendar = new GregorianCalendar();
+
+ /**
+ * The calendar with the selected day.
+ */
+ private GregorianCalendar calendar = new GregorianCalendar();
+
+ /**
+ * The action to perform when the user changes the value of the calendar.
+ */
+ private TAction updateAction = null;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param updateAction action to call when the user changes the value of
+ * the calendar
+ */
+ public TCalendar(final TWidget parent, final int x, final int y,
+ final TAction updateAction) {
+
+ // Set parent and window
+ super(parent, x, y, 28, 8);
+
+ this.updateAction = updateAction;
+ }
+
+ // ------------------------------------------------------------------------
+ // Event handlers ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Returns true if the mouse is currently on the left arrow.
+ *
+ * @param mouse mouse event
+ * @return true if the mouse is currently on the left arrow
+ */
+ private boolean mouseOnLeftArrow(final TMouseEvent mouse) {
+ if ((mouse.getY() == 0)
+ && (mouse.getX() == 1)
+ ) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the mouse is currently on the right arrow.
+ *
+ * @param mouse mouse event
+ * @return true if the mouse is currently on the right arrow
+ */
+ private boolean mouseOnRightArrow(final TMouseEvent mouse) {
+ if ((mouse.getY() == 0)
+ && (mouse.getX() == getWidth() - 2)
+ ) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Handle mouse down clicks.
+ *
+ * @param mouse mouse button down event
+ */
+ @Override
+ public void onMouseDown(final TMouseEvent mouse) {
+ if ((mouseOnLeftArrow(mouse)) && (mouse.isMouse1())) {
+ displayCalendar.add(Calendar.MONTH, -1);
+ } else if ((mouseOnRightArrow(mouse)) && (mouse.isMouse1())) {
+ displayCalendar.add(Calendar.MONTH, 1);
+ } else if (mouse.isMouse1()) {
+ // Find the day this might correspond to, and set it.
+ int index = (mouse.getY() - 2) * 7 + (mouse.getX() / 4) + 1;
+ // System.err.println("index: " + index);
+
+ int lastDayNumber = displayCalendar.getActualMaximum(
+ Calendar.DAY_OF_MONTH);
+ GregorianCalendar firstOfMonth = new GregorianCalendar();
+ firstOfMonth.setTimeInMillis(displayCalendar.getTimeInMillis());
+ firstOfMonth.set(Calendar.DAY_OF_MONTH, 1);
+ int dayOf1st = firstOfMonth.get(Calendar.DAY_OF_WEEK) - 1;
+ // System.err.println("dayOf1st: " + dayOf1st);
+
+ int day = index - dayOf1st;
+ // System.err.println("day: " + day);
+
+ if ((day < 1) || (day > lastDayNumber)) {
+ return;
+ }
+ calendar.setTimeInMillis(displayCalendar.getTimeInMillis());
+ calendar.set(Calendar.DAY_OF_MONTH, day);
+ }
+ }
+
+ /**
+ * Handle mouse double click.
+ *
+ * @param mouse mouse double click event
+ */
+ @Override
+ public void onMouseDoubleClick(final TMouseEvent mouse) {
+ if (updateAction != null) {
+ updateAction.DO(this);
+ }
+ }
+
+ /**
+ * Handle keystrokes.
+ *
+ * @param keypress keystroke event
+ */
+ @Override
+ public void onKeypress(final TKeypressEvent keypress) {
+ int increment = 0;
+
+ if (keypress.equals(kbUp)) {
+ increment = -7;
+ } else if (keypress.equals(kbDown)) {
+ increment = 7;
+ } else if (keypress.equals(kbLeft)) {
+ increment = -1;
+ } else if (keypress.equals(kbRight)) {
+ increment = 1;
+ } else if (keypress.equals(kbEnter)) {
+ if (updateAction != null) {
+ updateAction.DO(this);
+ }
+ return;
+ } else {
+ // Pass to parent for the things we don't care about.
+ super.onKeypress(keypress);
+ return;
+ }
+
+ if (increment != 0) {
+ calendar.add(Calendar.DAY_OF_YEAR, increment);
+
+ if ((displayCalendar.get(Calendar.MONTH) != calendar.get(
+ Calendar.MONTH))
+ || (displayCalendar.get(Calendar.YEAR) != calendar.get(
+ Calendar.YEAR))
+ ) {
+ if (increment < 0) {
+ displayCalendar.add(Calendar.MONTH, -1);
+ } else {
+ displayCalendar.add(Calendar.MONTH, 1);
+ }
+ }
+ }
+
+ }
+
+ // ------------------------------------------------------------------------
+ // TWidget ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Draw the combobox down arrow.
+ */
+ @Override
+ public void draw() {
+ CellAttributes backgroundColor = getTheme().getColor(
+ "tcalendar.background");
+ CellAttributes dayColor = getTheme().getColor(
+ "tcalendar.day");
+ CellAttributes selectedDayColor = getTheme().getColor(
+ "tcalendar.day.selected");
+ CellAttributes arrowColor = getTheme().getColor(
+ "tcalendar.arrow");
+ CellAttributes titleColor = getTheme().getColor(
+ "tcalendar.title");
+
+ // Fill in the interior background
+ for (int i = 0; i < getHeight(); i++) {
+ hLineXY(0, i, getWidth(), ' ', backgroundColor);
+ }
+
+ // Draw the title
+ String title = String.format("%tB %tY", displayCalendar,
+ displayCalendar);
+ // This particular title is always single-width (see format string
+ // above), but for completeness let's treat it the same as every
+ // other window title string.
+ int titleLeft = (getWidth() - StringUtils.width(title) - 2) / 2;
+ putCharXY(titleLeft, 0, ' ', titleColor);
+ putStringXY(titleLeft + 1, 0, title, titleColor);
+ putCharXY(titleLeft + StringUtils.width(title) + 1, 0, ' ',
+ titleColor);
+
+ // Arrows
+ putCharXY(1, 0, GraphicsChars.LEFTARROW, arrowColor);
+ putCharXY(getWidth() - 2, 0, GraphicsChars.RIGHTARROW,
+ arrowColor);
+
+ /*
+ * Now draw out the days.
+ */
+ putStringXY(0, 1, " S M T W T F S ", dayColor);
+ int lastDayNumber = displayCalendar.getActualMaximum(
+ Calendar.DAY_OF_MONTH);
+ GregorianCalendar firstOfMonth = new GregorianCalendar();
+ firstOfMonth.setTimeInMillis(displayCalendar.getTimeInMillis());
+ firstOfMonth.set(Calendar.DAY_OF_MONTH, 1);
+ int dayOf1st = firstOfMonth.get(Calendar.DAY_OF_WEEK) - 1;
+ int dayColumn = dayOf1st * 4;
+ int row = 2;
+
+ int dayOfMonth = 1;
+ while (dayOfMonth <= lastDayNumber) {
+ if (dayColumn == 4 * 7) {
+ dayColumn = 0;
+ row++;
+ }
+ if ((dayOfMonth == calendar.get(Calendar.DAY_OF_MONTH))
+ && (displayCalendar.get(Calendar.MONTH) == calendar.get(
+ Calendar.MONTH))
+ && (displayCalendar.get(Calendar.YEAR) == calendar.get(
+ Calendar.YEAR))
+ ) {
+ putStringXY(dayColumn, row,
+ String.format(" %2d ", dayOfMonth), selectedDayColor);
+ } else {
+ putStringXY(dayColumn, row,
+ String.format(" %2d ", dayOfMonth), dayColor);
+ }
+ dayColumn += 4;
+ dayOfMonth++;
+ }
+
+ }
+
+ // ------------------------------------------------------------------------
+ // TCalendar --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get calendar value.
+ *
+ * @return the current calendar value (clone instance)
+ */
+ public Calendar getValue() {
+ return (Calendar) calendar.clone();
+ }
+
+ /**
+ * Set calendar value.
+ *
+ * @param calendar the new value to use
+ */
+ public final void setValue(final Calendar calendar) {
+ this.calendar.setTimeInMillis(calendar.getTimeInMillis());
+ }
+
+ /**
+ * Set calendar value.
+ *
+ * @param millis the millis to set to
+ */
+ public final void setValue(final long millis) {
+ this.calendar.setTimeInMillis(millis);
+ }
+
+ }
--- /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 static jexer.TKeypress.kbEnter;
+ import static jexer.TKeypress.kbEsc;
+ import static jexer.TKeypress.kbSpace;
+ import jexer.bits.CellAttributes;
+ import jexer.bits.GraphicsChars;
+ import jexer.bits.MnemonicString;
+ import jexer.bits.StringUtils;
+ import jexer.event.TKeypressEvent;
+ import jexer.event.TMouseEvent;
+
+ /**
+ * TCheckBox implements an on/off checkbox.
+ */
+ public class TCheckBox extends TWidget {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * CheckBox state, true means checked.
+ */
+ private boolean checked = false;
+
+ /**
+ * The shortcut and checkbox label.
+ */
+ private MnemonicString mnemonic;
+
+ /**
+ * If true, use the window's background color.
+ */
+ private boolean useWindowBackground = false;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param label label to display next to (right of) the checkbox
+ * @param checked initial check state
+ */
+ public TCheckBox(final TWidget parent, final int x, final int y,
+ final String label, final boolean checked) {
+
+ // Set parent and window
+ super(parent, x, y, StringUtils.width(label) + 4, 1);
+
+ mnemonic = new MnemonicString(label);
+ this.checked = checked;
+
+ setCursorVisible(true);
+ setCursorX(1);
+ }
+
+ // ------------------------------------------------------------------------
+ // Event handlers ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Returns true if the mouse is currently on the checkbox.
+ *
+ * @param mouse mouse event
+ * @return true if the mouse is currently on the checkbox
+ */
+ private boolean mouseOnCheckBox(final TMouseEvent mouse) {
+ if ((mouse.getY() == 0)
+ && (mouse.getX() >= 0)
+ && (mouse.getX() <= 2)
+ ) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Handle mouse checkbox presses.
+ *
+ * @param mouse mouse button down event
+ */
+ @Override
+ public void onMouseDown(final TMouseEvent mouse) {
+ if ((mouseOnCheckBox(mouse)) && (mouse.isMouse1())) {
+ // Switch state
+ checked = !checked;
+ }
+ }
+
+ /**
+ * Handle keystrokes.
+ *
+ * @param keypress keystroke event
+ */
+ @Override
+ public void onKeypress(final TKeypressEvent keypress) {
+ if (keypress.equals(kbSpace)
+ || keypress.equals(kbEnter)
+ ) {
+ checked = !checked;
+ return;
+ }
+
+ if (keypress.equals(kbEsc)) {
+ checked = false;
+ return;
+ }
+
+ // Pass to parent for the things we don't care about.
+ super.onKeypress(keypress);
+ }
+
+ // ------------------------------------------------------------------------
+ // TWidget ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Draw a checkbox with label.
+ */
+ @Override
+ public void draw() {
+ CellAttributes checkboxColor;
+ CellAttributes mnemonicColor;
+
+ if (isAbsoluteActive()) {
+ checkboxColor = getTheme().getColor("tcheckbox.active");
+ mnemonicColor = getTheme().getColor("tcheckbox.mnemonic.highlighted");
+ } else {
+ checkboxColor = getTheme().getColor("tcheckbox.inactive");
+ mnemonicColor = getTheme().getColor("tcheckbox.mnemonic");
+ }
+ if (useWindowBackground) {
+ CellAttributes background = getWindow().getBackground();
+ checkboxColor.setBackColor(background.getBackColor());
+ }
+
+ putCharXY(0, 0, '[', checkboxColor);
+ if (checked) {
+ putCharXY(1, 0, GraphicsChars.CHECK, checkboxColor);
+ } else {
+ putCharXY(1, 0, ' ', checkboxColor);
+ }
+ putCharXY(2, 0, ']', checkboxColor);
+ putStringXY(4, 0, mnemonic.getRawLabel(), checkboxColor);
+ if (mnemonic.getScreenShortcutIdx() >= 0) {
+ putCharXY(4 + mnemonic.getScreenShortcutIdx(), 0,
+ mnemonic.getShortcut(), mnemonicColor);
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // TCheckBox --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get checked value.
+ *
+ * @return if true, this is checked
+ */
+ public boolean isChecked() {
+ return checked;
+ }
+
+ /**
+ * Set checked value.
+ *
+ * @param checked new checked value.
+ */
+ public void setChecked(final boolean checked) {
+ this.checked = checked;
+ }
+
+ /**
+ * Get the mnemonic string for this checkbox.
+ *
+ * @return mnemonic string
+ */
+ public MnemonicString getMnemonic() {
+ return mnemonic;
+ }
+
+ }
--- /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.ArrayList;
+ import java.util.List;
+
+ import jexer.bits.CellAttributes;
+ import jexer.bits.GraphicsChars;
+ import jexer.event.TKeypressEvent;
+ import jexer.event.TMouseEvent;
+ import jexer.event.TResizeEvent;
+ import jexer.event.TResizeEvent.Type;
+ import static jexer.TKeypress.*;
+
+ /**
+ * TComboBox implements a combobox containing a drop-down list and edit
+ * field. Alt-Down can be used to show the drop-down.
+ */
+ public class TComboBox extends TWidget {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The list of items in the drop-down.
+ */
+ private TList list;
+
+ /**
+ * The edit field containing the value to return.
+ */
+ private TField field;
+
+ /**
+ * The action to perform when the user selects an item (clicks or enter).
+ */
+ private TAction updateAction = null;
+
+ /**
+ * If true, the field cannot be updated to a value not on the list.
+ */
+ private boolean limitToListValue = true;
+
+ /**
+ * The height of the list of values when it is shown, or -1 to use the
+ * number of values in the list as the height.
+ */
+ private int valuesHeight = -1;
+
+ /**
+ * The values shown by the drop-down list.
+ */
+ private List<String> values = new ArrayList<String>();
+
+ /**
+ * When looking for a link between the displayed text and the list
+ * of values, do a case sensitive search.
+ */
+ private boolean caseSensitive = true;
+
+ /**
+ * The maximum height of the values drop-down when it is visible.
+ */
+ private int maxValuesHeight = 3;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width visible combobox width, including the down-arrow
+ * @param values the possible values for the box, shown in the drop-down
+ * @param valuesIndex the initial index in values, or -1 for no default
+ * value
+ * @param valuesHeight the height of the values drop-down when it is
+ * visible, or -1 to use the number of values as the height of the list
+ * @param updateAction action to call when a new value is selected from
+ * the list or enter is pressed in the edit field
+ */
+ public TComboBox(final TWidget parent, final int x, final int y,
+ final int width, final List<String> values, final int valuesIndex,
+ final int valuesHeight, final TAction updateAction) {
+
+ // Set parent and window
+ super(parent, x, y, width, 1);
+
+ assert (values != null);
+
+ this.updateAction = updateAction;
+ this.values = values;
+ this.valuesHeight = valuesHeight;
+
+ field = new TField(this, 0, 0, Math.max(0, width - 3), false, "",
+ updateAction, null);
+ if (valuesIndex >= 0) {
+ field.setText(values.get(valuesIndex));
+ }
+
+ setHeight(1);
+ if (limitToListValue) {
+ field.setEnabled(false);
+ } else {
+ activate(field);
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // Event handlers ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Returns true if the mouse is currently on the down arrow.
+ *
+ * @param mouse mouse event
+ * @return true if the mouse is currently on the down arrow
+ */
+ private boolean mouseOnArrow(final TMouseEvent mouse) {
+ if ((mouse.getY() == 0)
+ && (mouse.getX() >= getWidth() - 3)
+ && (mouse.getX() <= getWidth() - 1)
+ ) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Handle mouse down clicks.
+ *
+ * @param mouse mouse button down event
+ */
+ @Override
+ public void onMouseDown(final TMouseEvent mouse) {
+ if ((mouseOnArrow(mouse)) && (mouse.isMouse1())) {
+ // Make the list visible or not.
+ if (list != null) {
+ hideDropdown();
+ } else {
+ displayDropdown();
+ }
+ }
+
+ // Pass to parent for the things we don't care about.
+ super.onMouseDown(mouse);
+ }
+
+ /**
+ * Handle keystrokes.
+ *
+ * @param keypress keystroke event
+ */
+ @Override
+ public void onKeypress(final TKeypressEvent keypress) {
+ if (keypress.equals(kbEsc)) {
+ if (list != null) {
+ hideDropdown();
+ return;
+ }
+ }
+
+ if (keypress.equals(kbAltDown)) {
+ displayDropdown();
+ return;
+ }
+
+ if (keypress.equals(kbTab)
+ || (keypress.equals(kbShiftTab))
+ || (keypress.equals(kbBackTab))
+ ) {
+ if (list != null) {
+ hideDropdown();
+ return;
+ }
+ }
+
+ // Pass to parent for the things we don't care about.
+ super.onKeypress(keypress);
+ }
+
+ // ------------------------------------------------------------------------
+ // TWidget ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Override TWidget's width: we need to set child widget widths.
+ *
+ * @param width new widget width
+ */
+ @Override
+ public void setWidth(final int width) {
+ if (field != null) {
+ field.setWidth(width - 3);
+ }
+ if (list != null) {
+ list.setWidth(width);
+ }
+ super.setWidth(width);
+ }
+
+ /**
+ * Override TWidget's height: we can only set height at construction
+ * time.
+ *
+ * @param height new widget height (ignored)
+ */
+ @Override
+ public void setHeight(final int height) {
+ // Do nothing
+ }
+
+ /**
+ * Draw the combobox down arrow.
+ */
+ @Override
+ public void draw() {
+ CellAttributes comboBoxColor;
+
+ if (!isAbsoluteActive()) {
+ // We lost focus, turn off the list.
+ hideDropdown();
+ }
+
+ if (isAbsoluteActive()) {
+ comboBoxColor = getTheme().getColor("tcombobox.active");
+ } else {
+ comboBoxColor = getTheme().getColor("tcombobox.inactive");
+ }
+
+ putCharXY(getWidth() - 3, 0, GraphicsChars.DOWNARROWLEFT,
+ comboBoxColor);
+ putCharXY(getWidth() - 2, 0, GraphicsChars.DOWNARROW,
+ comboBoxColor);
+ putCharXY(getWidth() - 1, 0, GraphicsChars.DOWNARROWRIGHT,
+ comboBoxColor);
+ }
+
+ // ------------------------------------------------------------------------
+ // TComboBox --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Hide the drop-down list.
+ */
+ public void hideList() {
+ list.setEnabled(false);
+ list.setVisible(false);
+ super.setHeight(1);
+ if (limitToListValue == false) {
+ activate(field);
+ }
+ }
+
+ /**
+ * Show the drop-down list.
+ */
+ public void showList() {
+ list.setEnabled(true);
+ list.setVisible(true);
+ super.setHeight(list.getHeight() + 1);
+ activate(list);
+ }
+
+ /**
+ * Get combobox text value.
+ *
+ * @return text in the edit field
+ */
+ public String getText() {
+ return field.getText();
+ }
+
+ /**
+ * Set combobox text value.
+ *
+ * @param text the new text in the edit field
+ */
+ public void setText(final String text) {
+ setText(text, true);
+ }
+
+ /**
+ * Set combobox text value.
+ *
+ * @param text the new text in the edit field
+ * @param caseSensitive if true, perform a case-sensitive search for the
+ * list item
+ */
+ public void setText(final String text, final boolean caseSensitive) {
+ this.caseSensitive = caseSensitive;
+ field.setText(text);
+ if (list != null) {
+ displayDropdown();
+ }
+ }
+
+ /**
+ * Set combobox text to one of the list values.
+ *
+ * @param index the index in the list
+ */
+ public void setIndex(final int index) {
+ list.setSelectedIndex(index);
+ field.setText(list.getSelected());
+ }
+
+ /**
+ * Get a copy of the list of strings to display.
+ *
+ * @return the list of strings
+ */
+ public final List<String> getList() {
+ return list.getList();
+ }
+
+ /**
+ * Set the new list of strings to display.
+ *
+ * @param list new list of strings
+ */
+ public final void setList(final List<String> list) {
+ this.list.setList(list);
+ this.list.setHeight(Math.max(3, Math.min(list.size() + 1,
+ maxValuesHeight)));
+ field.setText("");
+ }
+
+ /**
+ * Make sure the widget displays all its elements correctly according to
+ * the current size and content.
+ */
+ public void reflowData() {
+ // TODO: why setW/setH/reflow not enough for the scrollbars?
+ TList list = this.list;
+ if (list != null) {
+ int valuesHeight = this.valuesHeight;
+ if (valuesHeight < 0) {
+ valuesHeight = values == null ? 0 : values.size() + 1;
+ }
+
+ list.onResize(new TResizeEvent(Type.WIDGET, getWidth(),
+ valuesHeight));
+ setHeight(valuesHeight + 1);
+ }
+
+ field.onResize(new TResizeEvent(Type.WIDGET, getWidth(),
+ field.getHeight()));
+ }
+
+ @Override
+ public void onResize(TResizeEvent resize) {
+ super.onResize(resize);
+ reflowData();
+ }
+
+ /**
+ * Display the drop-down menu represented by {@link TComboBox#list}.
+ */
+ private void displayDropdown() {
+ if (this.list != null) {
+ hideDropdown();
+ }
+
+ int valuesHeight = this.valuesHeight;
+ if (valuesHeight < 0) {
+ valuesHeight = values == null ? 0 : values.size() + 1;
+ }
+
+ TList list = new TList(this, values, 0, 1, getWidth(), valuesHeight,
+ new TAction() {
+ @Override
+ public void DO() {
+ TList list = TComboBox.this.list;
+ if (list == null) {
+ return;
+ }
+
+ field.setText(list.getSelected());
+ hideDropdown();
+
+ if (updateAction != null) {
+ updateAction.DO();
+ }
+ }
+ }
+ );
+
+ int i = -1;
+ if (values != null) {
+ String current = field.getText();
+ for (i = 0 ; i < values.size() ; i++) {
+ String value = values.get(i);
+ if ((caseSensitive && current.equals(value))
+ || (!caseSensitive && current.equalsIgnoreCase(value))) {
+ break;
+ }
+ }
+
+ if (i >= values.size()) {
+ i = -1;
+ }
+ }
+ list.setSelectedIndex(i);
+
+ list.setEnabled(true);
+ list.setVisible(true);
+
+ this.list = list;
+
+ reflowData();
+ activate(list);
+ }
+
+ /**
+ * Hide the drop-down menu represented by {@link TComboBox#list}.
+ */
+ private void hideDropdown() {
+ TList list = this.list;
+
+ if (list != null) {
+ list.setEnabled(false);
+ list.setVisible(false);
+ removeChild(list);
+
+ setHeight(1);
+ if (limitToListValue == false) {
+ activate(field);
+ }
+
+ this.list = 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;
+
+ /**
+ * This class encapsulates a user command event. User commands can be
+ * generated by menu actions, keyboard accelerators, and other UI elements.
+ * Commands can operate on both the application and individual widgets.
+ */
+ public class TCommand {
+
+ // ------------------------------------------------------------------------
+ // Constants --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Immediately abort the application (e.g. remote side closed
+ * connection).
+ */
+ public static final int ABORT = 1;
+
+ /**
+ * File open dialog.
+ */
+ public static final int OPEN = 2;
+
+ /**
+ * Exit application.
+ */
+ public static final int EXIT = 3;
+
+ /**
+ * Spawn OS shell window.
+ */
+ public static final int SHELL = 4;
+
+ /**
+ * Cut selected text and copy to the clipboard.
+ */
+ public static final int CUT = 5;
+
+ /**
+ * Copy selected text to clipboard.
+ */
+ public static final int COPY = 6;
+
+ /**
+ * Paste from clipboard.
+ */
+ public static final int PASTE = 7;
+
+ /**
+ * Clear selected text without copying it to the clipboard.
+ */
+ public static final int CLEAR = 8;
+
+ /**
+ * Tile windows.
+ */
+ public static final int TILE = 9;
+
+ /**
+ * Cascade windows.
+ */
+ public static final int CASCADE = 10;
+
+ /**
+ * Close all windows.
+ */
+ public static final int CLOSE_ALL = 11;
+
+ /**
+ * Move (move/resize) window.
+ */
+ public static final int WINDOW_MOVE = 12;
+
+ /**
+ * Zoom (maximize/restore) window.
+ */
+ public static final int WINDOW_ZOOM = 13;
+
+ /**
+ * Next window (like Alt-TAB).
+ */
+ public static final int WINDOW_NEXT = 14;
+
+ /**
+ * Previous window (like Shift-Alt-TAB).
+ */
+ public static final int WINDOW_PREVIOUS = 15;
+
+ /**
+ * Close window.
+ */
+ public static final int WINDOW_CLOSE = 16;
+
+ /**
+ * Enter help system.
+ */
+ public static final int HELP = 20;
+
+ /**
+ * Enter first menu.
+ */
+ public static final int MENU = 21;
+
+ /**
+ * Save file.
+ */
+ public static final int SAVE = 30;
+
+ /**
+ * Backend disconnected.
+ */
+ public static final int BACKEND_DISCONNECT = 100;
+
+ public static final TCommand cmAbort = new TCommand(ABORT);
+ public static final TCommand cmExit = new TCommand(EXIT);
+ public static final TCommand cmQuit = new TCommand(EXIT);
+ public static final TCommand cmOpen = new TCommand(OPEN);
+ public static final TCommand cmShell = new TCommand(SHELL);
+ public static final TCommand cmCut = new TCommand(CUT);
+ public static final TCommand cmCopy = new TCommand(COPY);
+ public static final TCommand cmPaste = new TCommand(PASTE);
+ public static final TCommand cmClear = new TCommand(CLEAR);
+ public static final TCommand cmTile = new TCommand(TILE);
+ public static final TCommand cmCascade = new TCommand(CASCADE);
+ public static final TCommand cmCloseAll = new TCommand(CLOSE_ALL);
+ public static final TCommand cmWindowMove = new TCommand(WINDOW_MOVE);
+ public static final TCommand cmWindowZoom = new TCommand(WINDOW_ZOOM);
+ public static final TCommand cmWindowNext = new TCommand(WINDOW_NEXT);
+ public static final TCommand cmWindowPrevious = new TCommand(WINDOW_PREVIOUS);
+ public static final TCommand cmWindowClose = new TCommand(WINDOW_CLOSE);
+ public static final TCommand cmHelp = new TCommand(HELP);
+ public static final TCommand cmSave = new TCommand(SAVE);
+ public static final TCommand cmMenu = new TCommand(MENU);
+ public static final TCommand cmBackendDisconnect = new TCommand(BACKEND_DISCONNECT);
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Type of command, one of EXIT, CASCADE, etc.
+ */
+ private int type;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param type the Type of command, one of EXIT, CASCADE, etc.
+ */
+ public TCommand(final int type) {
+ this.type = type;
+ }
+
+ // ------------------------------------------------------------------------
+ // TCommand ---------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Make human-readable description of this TCommand.
+ *
+ * @return displayable String
+ */
+ @Override
+ public final String toString() {
+ return String.format("%s", type);
+ }
+
+ /**
+ * Comparison check. All fields must match to return true.
+ *
+ * @param rhs another TCommand instance
+ * @return true if all fields are equal
+ */
+ @Override
+ public final boolean equals(final Object rhs) {
+ if (!(rhs instanceof TCommand)) {
+ return false;
+ }
+
+ TCommand that = (TCommand) rhs;
+ return (type == that.type);
+ }
+
+ /**
+ * Hashcode uses all fields in equals().
+ *
+ * @return the hash
+ */
+ @Override
+ public int hashCode() {
+ return type;
+ }
+
+ }
--- /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 jexer.bits.CellAttributes;
+ import jexer.bits.GraphicsChars;
+ import jexer.event.TKeypressEvent;
+ import jexer.event.TMenuEvent;
+ import jexer.event.TMouseEvent;
+ import jexer.event.TResizeEvent;
+
+ /**
+ * TDesktop is a special-class window that is drawn underneath everything
+ * else. Like a TWindow, it can contain widgets and perform "background"
+ * processing via onIdle(). But unlike a TWindow, it cannot be hidden,
+ * moved, or resized.
+ *
+ * <p>
+ * Events are passed to TDesktop as follows:
+ * <ul>
+ * <li>Mouse events are seen if they do not cover any other windows.</li>
+ * <li>Keypress events are seen if no other windows are open.</li>
+ * <li>Menu events are seen if no other windows are open.</li>
+ * <li>Command events are seen if no other windows are open.</li>
+ * </ul>
+ */
+ public class TDesktop extends TWindow {
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent application
+ */
+ public TDesktop(final TApplication parent) {
+ super(parent, "", 0, 0, parent.getScreen().getWidth(),
+ parent.getDesktopBottom() - parent.getDesktopTop());
+
+ setActive(false);
+ }
+
+ // ------------------------------------------------------------------------
+ // Event handlers ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Handle window/screen resize events.
+ *
+ * @param resize resize event
+ */
+ @Override
+ public void onResize(final TResizeEvent resize) {
+ if (getChildren().size() == 1) {
+ TWidget child = getChildren().get(0);
+ if (!(child instanceof TWindow)) {
+ // Only one child, resize it to match my size.
+ child.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
+ getWidth(), getHeight()));
+ }
+ }
+ if (resize.getType() == TResizeEvent.Type.SCREEN) {
+ // Let children see the screen resize
+ for (TWidget widget: getChildren()) {
+ widget.onResize(resize);
+ }
+ }
+ }
+
+ /**
+ * Handle mouse button presses.
+ *
+ * @param mouse mouse button event
+ */
+ @Override
+ public void onMouseDown(final TMouseEvent mouse) {
+ this.mouse = mouse;
+
+ // Pass to children
+ for (TWidget widget: getChildren()) {
+ if (widget.mouseWouldHit(mouse)) {
+ // Dispatch to this child, also activate it
+ activate(widget);
+
+ // Set x and y relative to the child's coordinates
+ mouse.setX(mouse.getAbsoluteX() - widget.getAbsoluteX());
+ mouse.setY(mouse.getAbsoluteY() - widget.getAbsoluteY());
+ widget.handleEvent(mouse);
+ return;
+ }
+ }
+ }
+
+ /**
+ * Handle mouse button releases.
+ *
+ * @param mouse mouse button release event
+ */
+ @Override
+ public void onMouseUp(final TMouseEvent mouse) {
+ this.mouse = mouse;
+
+ // Pass to children
+ for (TWidget widget: getChildren()) {
+ if (widget.mouseWouldHit(mouse)) {
+ // Dispatch to this child, also activate it
+ activate(widget);
+
+ // Set x and y relative to the child's coordinates
+ mouse.setX(mouse.getAbsoluteX() - widget.getAbsoluteX());
+ mouse.setY(mouse.getAbsoluteY() - widget.getAbsoluteY());
+ widget.handleEvent(mouse);
+ return;
+ }
+ }
+ }
+
+ /**
+ * Handle mouse movements.
+ *
+ * @param mouse mouse motion event
+ */
+ @Override
+ public void onMouseMotion(final TMouseEvent mouse) {
+ this.mouse = mouse;
+
+ // Default: do nothing, pass to children instead
+ super.onMouseMotion(mouse);
+ }
+
+ /**
+ * Handle keystrokes.
+ *
+ * @param keypress keystroke event
+ */
+ @Override
+ public void onKeypress(final TKeypressEvent keypress) {
+ // Default: do nothing, pass to children instead
+ super.onKeypress(keypress);
+ }
+
+ /**
+ * Handle posted menu events.
+ *
+ * @param menu menu event
+ */
+ @Override
+ public void onMenu(final TMenuEvent menu) {
+ // Default: do nothing, pass to children instead
+ super.onMenu(menu);
+ }
+
+ // ------------------------------------------------------------------------
+ // TWindow ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The default TDesktop draws a hatch character across everything.
+ */
+ @Override
+ public void draw() {
+ CellAttributes background = getTheme().getColor("tdesktop.background");
+ putAll(GraphicsChars.HATCH, background);
+
+ /*
+ // For debugging, let's see where the desktop bounds really are.
+ putCharXY(0, 0, '0', background);
+ putCharXY(getWidth() - 1, 0, '1', background);
+ putCharXY(0, getHeight() - 1, '2', background);
+ putCharXY(getWidth() - 1, getHeight() - 1, '3', background);
+ */
+ }
+
+ /**
+ * Hide window. This is a NOP for TDesktop.
+ */
+ @Override
+ public final void hide() {}
+
+ /**
+ * Show window. This is a NOP for TDesktop.
+ */
+ @Override
+ public final void show() {}
+
+ /**
+ * Called by hide(). This is a NOP for TDesktop.
+ */
+ @Override
+ public final void onHide() {}
+
+ /**
+ * Called by show(). This is a NOP for TDesktop.
+ */
+ @Override
+ public final void onShow() {}
+
+ /**
+ * Returns true if the mouse is currently on the close button.
+ *
+ * @return true if mouse is currently on the close button
+ */
+ @Override
+ protected final boolean mouseOnClose() {
+ return false;
+ }
+
+ /**
+ * Returns true if the mouse is currently on the maximize/restore button.
+ *
+ * @return true if the mouse is currently on the maximize/restore button
+ */
+ @Override
+ protected final boolean mouseOnMaximize() {
+ return false;
+ }
+
+ /**
+ * Returns true if the mouse is currently on the resizable lower right
+ * corner.
+ *
+ * @return true if the mouse is currently on the resizable lower right
+ * corner
+ */
+ @Override
+ protected final boolean mouseOnResize() {
+ return false;
+ }
+
+ }
--- /dev/null
+ /*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+ package jexer;
+
+ import java.io.File;
+ import java.util.ArrayList;
+ import java.util.HashMap;
+ import java.util.List;
+ import java.util.Map;
+
+ import jexer.bits.StringUtils;
+
+ /**
+ * TDirectoryList shows the files within a directory.
+ */
+ public class TDirectoryList extends TList {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Files in the directory.
+ */
+ private Map<String, File> files;
+
+ /**
+ * Root path containing files to display.
+ */
+ private File path;
+
+ /**
+ * The list of filters that a file must match in order to be displayed.
+ */
+ private List<String> filters;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param path directory path, must be a directory
+ * @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 TDirectoryList(final TWidget parent, final String path, final int x,
+ final int y, final int width, final int height) {
+
+ this(parent, path, x, y, width, height, null, null, null);
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param path directory path, must be a directory
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of text area
+ * @param height height of text area
+ * @param action action to perform when an item is selected (enter or
+ * double-click)
+ */
+ public TDirectoryList(final TWidget parent, final String path, final int x,
+ final int y, final int width, final int height, final TAction action) {
+
+ this(parent, path, x, y, width, height, action, null, null);
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param path directory path, must be a directory
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of text area
+ * @param height height of text area
+ * @param action action to perform when an item is selected (enter or
+ * double-click)
+ * @param singleClickAction action to perform when an item is selected
+ * (single-click)
+ */
+ public TDirectoryList(final TWidget parent, final String path, final int x,
+ final int y, final int width, final int height, final TAction action,
+ final TAction singleClickAction) {
+
+ this(parent, path, x, y, width, height, action, singleClickAction,
+ null);
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param path directory path, must be a directory
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of text area
+ * @param height height of text area
+ * @param action action to perform when an item is selected (enter or
+ * double-click)
+ * @param singleClickAction action to perform when an item is selected
+ * (single-click)
+ * @param filters a list of strings that files must match to be displayed
+ */
+ public TDirectoryList(final TWidget parent, final String path, final int x,
+ final int y, final int width, final int height, final TAction action,
+ final TAction singleClickAction, final List<String> filters) {
+
+ super(parent, null, x, y, width, height, action);
+ files = new HashMap<String, File>();
+ this.filters = filters;
+ this.singleClickAction = singleClickAction;
+
+ setPath(path);
+ }
+
+ // ------------------------------------------------------------------------
+ // TList ------------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ // ------------------------------------------------------------------------
+ // TDirectoryList ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Set the new path to display.
+ *
+ * @param path new path to list files for
+ */
+ public void setPath(final String path) {
+ this.path = new File(path);
+
+ List<String> newStrings = new ArrayList<String>();
+ files.clear();
+
+ // Build a list of files in this directory
+ File [] newFiles = this.path.listFiles();
+ if (newFiles != null) {
+ for (int i = 0; i < newFiles.length; i++) {
+ if (newFiles[i].getName().startsWith(".")) {
+ continue;
+ }
+ if (newFiles[i].isDirectory()) {
+ continue;
+ }
+ if (filters != null) {
+ for (String pattern: filters) {
+
+ /*
+ System.err.println("newFiles[i] " +
+ newFiles[i].getName() + " " + pattern +
+ " " + newFiles[i].getName().matches(pattern));
+ */
+
+ if (newFiles[i].getName().matches(pattern)) {
+ String key = renderFile(newFiles[i]);
+ files.put(key, newFiles[i]);
+ newStrings.add(key);
+ break;
+ }
+ }
+ } else {
+ String key = renderFile(newFiles[i]);
+ files.put(key, newFiles[i]);
+ newStrings.add(key);
+ }
+ }
+ }
+ setList(newStrings);
+
+ // Select the first entry
+ if (getMaxSelectedIndex() >= 0) {
+ setSelectedIndex(0);
+ }
+ }
+
+ /**
+ * Get the path that is being displayed.
+ *
+ * @return the path
+ */
+ public File getPath() {
+ path = files.get(getSelected());
+ return path;
+ }
+
+ /**
+ * Format one of the entries for drawing on the screen.
+ *
+ * @param file the File
+ * @return the line to draw
+ */
+ private String renderFile(final File file) {
+ String name = file.getName();
+ if (StringUtils.width(name) > 20) {
+ name = name.substring(0, 17) + "...";
+ }
+ return String.format("%-20s %5dk", name, (file.length() / 1024));
+ }
+
+ }
--- /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.List;
+ import java.util.ResourceBundle;
+
+ import jexer.bits.Color;
+ import jexer.bits.ColorTheme;
+ import jexer.bits.CellAttributes;
+ import jexer.bits.GraphicsChars;
+ import jexer.event.TKeypressEvent;
+ import jexer.event.TMouseEvent;
+ import static jexer.TKeypress.*;
+
+ /**
+ * TEditColorThemeWindow provides an easy UI for users to alter the running
+ * color theme.
+ *
+ */
+ public class TEditColorThemeWindow extends TWindow {
+
+ /**
+ * Translated strings.
+ */
+ private static final ResourceBundle i18n = ResourceBundle.getBundle(TEditColorThemeWindow.class.getName());
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The current editing theme.
+ */
+ private ColorTheme editTheme;
+
+ /**
+ * The left-side list of colors pane.
+ */
+ private TList colorNames;
+
+ /**
+ * The foreground color.
+ */
+ private ForegroundPicker foreground;
+
+ /**
+ * The background color.
+ */
+ private BackgroundPicker background;
+
+ /**
+ * The foreground color picker.
+ */
+ class ForegroundPicker extends TWidget {
+
+ /**
+ * The selected color.
+ */
+ Color color;
+
+ /**
+ * The bold flag.
+ */
+ boolean bold;
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @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 ForegroundPicker(final TWidget parent, final int x,
+ final int y, final int width, final int height) {
+
+ super(parent, x, y, width, height);
+ }
+
+ /**
+ * Get the X grid coordinate for this color.
+ *
+ * @param color the Color value
+ * @return the X coordinate
+ */
+ private int getXColorPosition(final Color color) {
+ if (color.equals(Color.BLACK)) {
+ return 2;
+ } else if (color.equals(Color.BLUE)) {
+ return 5;
+ } else if (color.equals(Color.GREEN)) {
+ return 8;
+ } else if (color.equals(Color.CYAN)) {
+ return 11;
+ } else if (color.equals(Color.RED)) {
+ return 2;
+ } else if (color.equals(Color.MAGENTA)) {
+ return 5;
+ } else if (color.equals(Color.YELLOW)) {
+ return 8;
+ } else if (color.equals(Color.WHITE)) {
+ return 11;
+ }
+ throw new IllegalArgumentException("Invalid color: " + color);
+ }
+
+ /**
+ * Get the Y grid coordinate for this color.
+ *
+ * @param color the Color value
+ * @param bold if true use bold color
+ * @return the Y coordinate
+ */
+ private int getYColorPosition(final Color color, final boolean bold) {
+ int dotY = 1;
+ if (color.equals(Color.RED)) {
+ dotY = 2;
+ } else if (color.equals(Color.MAGENTA)) {
+ dotY = 2;
+ } else if (color.equals(Color.YELLOW)) {
+ dotY = 2;
+ } else if (color.equals(Color.WHITE)) {
+ dotY = 2;
+ }
+ if (bold) {
+ dotY += 2;
+ }
+ return dotY;
+ }
+
+ /**
+ * Get the bold value based on Y grid coordinate.
+ *
+ * @param dotY the Y coordinate
+ * @return the bold value
+ */
+ private boolean getBoldFromPosition(final int dotY) {
+ if (dotY > 2) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Get the color based on (X, Y) grid coordinate.
+ *
+ * @param dotX the X coordinate
+ * @param dotY the Y coordinate
+ * @return the Color value
+ */
+ private Color getColorFromPosition(final int dotX, final int dotY) {
+ int y = dotY;
+ if (y > 2) {
+ y -= 2;
+ }
+ if ((1 <= dotX) && (dotX <= 3) && (y == 1)) {
+ return Color.BLACK;
+ }
+ if ((4 <= dotX) && (dotX <= 6) && (y == 1)) {
+ return Color.BLUE;
+ }
+ if ((7 <= dotX) && (dotX <= 9) && (y == 1)) {
+ return Color.GREEN;
+ }
+ if ((10 <= dotX) && (dotX <= 12) && (y == 1)) {
+ return Color.CYAN;
+ }
+ if ((1 <= dotX) && (dotX <= 3) && (y == 2)) {
+ return Color.RED;
+ }
+ if ((4 <= dotX) && (dotX <= 6) && (y == 2)) {
+ return Color.MAGENTA;
+ }
+ if ((7 <= dotX) && (dotX <= 9) && (y == 2)) {
+ return Color.YELLOW;
+ }
+ if ((10 <= dotX) && (dotX <= 12) && (y == 2)) {
+ return Color.WHITE;
+ }
+
+ throw new IllegalArgumentException("Invalid coordinates: "
+ + dotX + ", " + dotY);
+ }
+
+ /**
+ * Draw the foreground colors grid.
+ */
+ @Override
+ public void draw() {
+ CellAttributes border = getWindow().getBorder();
+ CellAttributes background = getWindow().getBackground();
+ CellAttributes attr = new CellAttributes();
+
+ drawBox(0, 0, getWidth(), getHeight(), border, background, 1,
+ false);
+
+ attr.setTo(getTheme().getColor("twindow.background.modal"));
+ if (isActive()) {
+ attr.setForeColor(getTheme().getColor("tlabel").getForeColor());
+ attr.setBold(getTheme().getColor("tlabel").isBold());
+ }
+ putStringXY(1, 0, i18n.getString("foregroundLabel"), attr);
+
+ // Have to draw the colors manually because the int value matches
+ // SGR, not CGA.
+ attr.reset();
+ attr.setForeColor(Color.BLACK);
+ putStringXY(1, 1, "\u2588\u2588\u2588", attr);
+ attr.setForeColor(Color.BLUE);
+ putStringXY(4, 1, "\u2588\u2588\u2588", attr);
+ attr.setForeColor(Color.GREEN);
+ putStringXY(7, 1, "\u2588\u2588\u2588", attr);
+ attr.setForeColor(Color.CYAN);
+ putStringXY(10, 1, "\u2588\u2588\u2588", attr);
+ attr.setForeColor(Color.RED);
+ putStringXY(1, 2, "\u2588\u2588\u2588", attr);
+ attr.setForeColor(Color.MAGENTA);
+ putStringXY(4, 2, "\u2588\u2588\u2588", attr);
+ attr.setForeColor(Color.YELLOW);
+ putStringXY(7, 2, "\u2588\u2588\u2588", attr);
+ attr.setForeColor(Color.WHITE);
+ putStringXY(10, 2, "\u2588\u2588\u2588", attr);
+
+ attr.setBold(true);
+ attr.setForeColor(Color.BLACK);
+ putStringXY(1, 3, "\u2588\u2588\u2588", attr);
+ attr.setForeColor(Color.BLUE);
+ putStringXY(4, 3, "\u2588\u2588\u2588", attr);
+ attr.setForeColor(Color.GREEN);
+ putStringXY(7, 3, "\u2588\u2588\u2588", attr);
+ attr.setForeColor(Color.CYAN);
+ putStringXY(10, 3, "\u2588\u2588\u2588", attr);
+ attr.setForeColor(Color.RED);
+ putStringXY(1, 4, "\u2588\u2588\u2588", attr);
+ attr.setForeColor(Color.MAGENTA);
+ putStringXY(4, 4, "\u2588\u2588\u2588", attr);
+ attr.setForeColor(Color.YELLOW);
+ putStringXY(7, 4, "\u2588\u2588\u2588", attr);
+ attr.setForeColor(Color.WHITE);
+ putStringXY(10, 4, "\u2588\u2588\u2588", attr);
+
+ // Draw the dot
+ int dotX = getXColorPosition(color);
+ int dotY = getYColorPosition(color, bold);
+ if (color.equals(Color.BLACK) && !bold) {
+ // Use white-on-black for black. All other colors use
+ // black-on-whatever.
+ attr.reset();
+ putCharXY(dotX, dotY, GraphicsChars.CP437[0x07], attr);
+ } else {
+ attr.setForeColor(color);
+ attr.setBold(bold);
+ putCharXY(dotX, dotY, '\u25D8', attr);
+ }
+ }
+
+ /**
+ * Handle keystrokes.
+ *
+ * @param keypress keystroke event
+ */
+ @Override
+ public void onKeypress(final TKeypressEvent keypress) {
+ if (keypress.equals(kbRight)) {
+ int dotX = getXColorPosition(color);
+ int dotY = getYColorPosition(color, bold);
+ if (dotX < 10) {
+ dotX += 3;
+ }
+ color = getColorFromPosition(dotX, dotY);
+ } else if (keypress.equals(kbLeft)) {
+ int dotX = getXColorPosition(color);
+ int dotY = getYColorPosition(color, bold);
+ if (dotX > 3) {
+ dotX -= 3;
+ }
+ color = getColorFromPosition(dotX, dotY);
+ } else if (keypress.equals(kbUp)) {
+ int dotX = getXColorPosition(color);
+ int dotY = getYColorPosition(color, bold);
+ if (dotY > 1) {
+ dotY--;
+ }
+ color = getColorFromPosition(dotX, dotY);
+ bold = getBoldFromPosition(dotY);
+ } else if (keypress.equals(kbDown)) {
+ int dotX = getXColorPosition(color);
+ int dotY = getYColorPosition(color, bold);
+ if (dotY < 4) {
+ dotY++;
+ }
+ color = getColorFromPosition(dotX, dotY);
+ bold = getBoldFromPosition(dotY);
+ } else {
+ // Pass to my parent
+ super.onKeypress(keypress);
+ return;
+ }
+
+ // Save this update to the local theme.
+ ((TEditColorThemeWindow) getWindow()).saveToEditTheme();
+ }
+
+ /**
+ * Handle mouse press events.
+ *
+ * @param mouse mouse button press event
+ */
+ @Override
+ public void onMouseDown(final TMouseEvent mouse) {
+ if (mouse.isMouseWheelUp()) {
+ // Do this like kbUp
+ int dotX = getXColorPosition(color);
+ int dotY = getYColorPosition(color, bold);
+ if (dotY > 1) {
+ dotY--;
+ }
+ color = getColorFromPosition(dotX, dotY);
+ bold = getBoldFromPosition(dotY);
+ } else if (mouse.isMouseWheelDown()) {
+ // Do this like kbDown
+ int dotX = getXColorPosition(color);
+ int dotY = getYColorPosition(color, bold);
+ if (dotY < 4) {
+ dotY++;
+ }
+ color = getColorFromPosition(dotX, dotY);
+ bold = getBoldFromPosition(dotY);
+ } else if ((mouse.getX() > 0)
+ && (mouse.getX() < getWidth() - 1)
+ && (mouse.getY() > 0)
+ && (mouse.getY() < getHeight() - 1)
+ ) {
+ color = getColorFromPosition(mouse.getX(), mouse.getY());
+ bold = getBoldFromPosition(mouse.getY());
+ } else {
+ // Let parent class handle it.
+ super.onMouseDown(mouse);
+ return;
+ }
+
+ // Save this update to the local theme.
+ ((TEditColorThemeWindow) getWindow()).saveToEditTheme();
+ }
+
+ }
+
+ /**
+ * The background color picker.
+ */
+ class BackgroundPicker extends TWidget {
+
+ /**
+ * The selected color.
+ */
+ Color color;
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @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 BackgroundPicker(final TWidget parent, final int x,
+ final int y, final int width, final int height) {
+
+ super(parent, x, y, width, height);
+ }
+
+ /**
+ * Get the X grid coordinate for this color.
+ *
+ * @param color the Color value
+ * @return the X coordinate
+ */
+ private int getXColorPosition(final Color color) {
+ if (color.equals(Color.BLACK)) {
+ return 2;
+ } else if (color.equals(Color.BLUE)) {
+ return 5;
+ } else if (color.equals(Color.GREEN)) {
+ return 8;
+ } else if (color.equals(Color.CYAN)) {
+ return 11;
+ } else if (color.equals(Color.RED)) {
+ return 2;
+ } else if (color.equals(Color.MAGENTA)) {
+ return 5;
+ } else if (color.equals(Color.YELLOW)) {
+ return 8;
+ } else if (color.equals(Color.WHITE)) {
+ return 11;
+ }
+ throw new IllegalArgumentException("Invalid color: " + color);
+ }
+
+ /**
+ * Get the Y grid coordinate for this color.
+ *
+ * @param color the Color value
+ * @return the Y coordinate
+ */
+ private int getYColorPosition(final Color color) {
+ int dotY = 1;
+ if (color.equals(Color.RED)) {
+ dotY = 2;
+ } else if (color.equals(Color.MAGENTA)) {
+ dotY = 2;
+ } else if (color.equals(Color.YELLOW)) {
+ dotY = 2;
+ } else if (color.equals(Color.WHITE)) {
+ dotY = 2;
+ }
+ return dotY;
+ }
+
+ /**
+ * Get the color based on (X, Y) grid coordinate.
+ *
+ * @param dotX the X coordinate
+ * @param dotY the Y coordinate
+ * @return the Color value
+ */
+ private Color getColorFromPosition(final int dotX, final int dotY) {
+ if ((1 <= dotX) && (dotX <= 3) && (dotY == 1)) {
+ return Color.BLACK;
+ }
+ if ((4 <= dotX) && (dotX <= 6) && (dotY == 1)) {
+ return Color.BLUE;
+ }
+ if ((7 <= dotX) && (dotX <= 9) && (dotY == 1)) {
+ return Color.GREEN;
+ }
+ if ((10 <= dotX) && (dotX <= 12) && (dotY == 1)) {
+ return Color.CYAN;
+ }
+ if ((1 <= dotX) && (dotX <= 3) && (dotY == 2)) {
+ return Color.RED;
+ }
+ if ((4 <= dotX) && (dotX <= 6) && (dotY == 2)) {
+ return Color.MAGENTA;
+ }
+ if ((7 <= dotX) && (dotX <= 9) && (dotY == 2)) {
+ return Color.YELLOW;
+ }
+ if ((10 <= dotX) && (dotX <= 12) && (dotY == 2)) {
+ return Color.WHITE;
+ }
+
+ throw new IllegalArgumentException("Invalid coordinates: "
+ + dotX + ", " + dotY);
+ }
+
+ /**
+ * Draw the background colors grid.
+ */
+ @Override
+ public void draw() {
+ CellAttributes border = getWindow().getBorder();
+ CellAttributes background = getWindow().getBackground();
+ CellAttributes attr = new CellAttributes();
+
+ drawBox(0, 0, getWidth(), getHeight(), border, background, 1,
+ false);
+
+ attr.setTo(getTheme().getColor("twindow.background.modal"));
+ if (isActive()) {
+ attr.setForeColor(getTheme().getColor("tlabel").getForeColor());
+ attr.setBold(getTheme().getColor("tlabel").isBold());
+ }
+ putStringXY(1, 0, i18n.getString("backgroundLabel"), attr);
+
+ // Have to draw the colors manually because the int value matches
+ // SGR, not CGA.
+ attr.reset();
+ attr.setForeColor(Color.BLACK);
+ putStringXY(1, 1, "\u2588\u2588\u2588", attr);
+ attr.setForeColor(Color.BLUE);
+ putStringXY(4, 1, "\u2588\u2588\u2588", attr);
+ attr.setForeColor(Color.GREEN);
+ putStringXY(7, 1, "\u2588\u2588\u2588", attr);
+ attr.setForeColor(Color.CYAN);
+ putStringXY(10, 1, "\u2588\u2588\u2588", attr);
+ attr.setForeColor(Color.RED);
+ putStringXY(1, 2, "\u2588\u2588\u2588", attr);
+ attr.setForeColor(Color.MAGENTA);
+ putStringXY(4, 2, "\u2588\u2588\u2588", attr);
+ attr.setForeColor(Color.YELLOW);
+ putStringXY(7, 2, "\u2588\u2588\u2588", attr);
+ attr.setForeColor(Color.WHITE);
+ putStringXY(10, 2, "\u2588\u2588\u2588", attr);
+
+ // Draw the dot
+ int dotX = getXColorPosition(color);
+ int dotY = getYColorPosition(color);
+ if (color.equals(Color.BLACK)) {
+ // Use white-on-black for black. All other colors use
+ // black-on-whatever.
+ attr.reset();
+ putCharXY(dotX, dotY, GraphicsChars.CP437[0x07], attr);
+ } else {
+ attr.setForeColor(color);
+ putCharXY(dotX, dotY, '\u25D8', attr);
+ }
+
+ }
+
+ /**
+ * Handle keystrokes.
+ *
+ * @param keypress keystroke event
+ */
+ @Override
+ public void onKeypress(final TKeypressEvent keypress) {
+ if (keypress.equals(kbRight)) {
+ int dotX = getXColorPosition(color);
+ int dotY = getYColorPosition(color);
+ if (dotX < 10) {
+ dotX += 3;
+ }
+ color = getColorFromPosition(dotX, dotY);
+ } else if (keypress.equals(kbLeft)) {
+ int dotX = getXColorPosition(color);
+ int dotY = getYColorPosition(color);
+ if (dotX > 3) {
+ dotX -= 3;
+ }
+ color = getColorFromPosition(dotX, dotY);
+ } else if (keypress.equals(kbUp)) {
+ int dotX = getXColorPosition(color);
+ int dotY = getYColorPosition(color);
+ if (dotY == 2) {
+ dotY--;
+ }
+ color = getColorFromPosition(dotX, dotY);
+ } else if (keypress.equals(kbDown)) {
+ int dotX = getXColorPosition(color);
+ int dotY = getYColorPosition(color);
+ if (dotY == 1) {
+ dotY++;
+ }
+ color = getColorFromPosition(dotX, dotY);
+ } else {
+ // Pass to my parent
+ super.onKeypress(keypress);
+ }
+
+ // Save this update to the local theme.
+ ((TEditColorThemeWindow) getWindow()).saveToEditTheme();
+ }
+
+ /**
+ * Handle mouse press events.
+ *
+ * @param mouse mouse button press event
+ */
+ @Override
+ public void onMouseDown(final TMouseEvent mouse) {
+ if (mouse.isMouseWheelUp()) {
+ // Do this like kbUp
+ int dotX = getXColorPosition(color);
+ int dotY = getYColorPosition(color);
+ if (dotY == 2) {
+ dotY--;
+ }
+ color = getColorFromPosition(dotX, dotY);
+ } else if (mouse.isMouseWheelDown()) {
+ // Do this like kbDown
+ int dotX = getXColorPosition(color);
+ int dotY = getYColorPosition(color);
+ if (dotY == 1) {
+ dotY++;
+ }
+ color = getColorFromPosition(dotX, dotY);
+ return;
+ } else if ((mouse.getX() > 0)
+ && (mouse.getX() < getWidth() - 1)
+ && (mouse.getY() > 0)
+ && (mouse.getY() < getHeight() - 1)
+ ) {
+ color = getColorFromPosition(mouse.getX(), mouse.getY());
+ } else {
+ // Let parent class handle it.
+ super.onMouseDown(mouse);
+ return;
+ }
+
+ // Save this update to the local theme.
+ ((TEditColorThemeWindow) getWindow()).saveToEditTheme();
+ }
+
+ }
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor. The window will be centered on screen.
+ *
+ * @param application the TApplication that manages this window
+ */
+ public TEditColorThemeWindow(final TApplication application) {
+
+ // Register with the TApplication
+ super(application, i18n.getString("windowTitle"), 0, 0, 60, 18, MODAL);
+
+ // Initialize with the first color
+ List<String> colors = getTheme().getColorNames();
+ assert (colors.size() > 0);
+ editTheme = new ColorTheme();
+ for (String key: colors) {
+ CellAttributes attr = new CellAttributes();
+ attr.setTo(getTheme().getColor(key));
+ editTheme.setColor(key, attr);
+ }
+
+ colorNames = addList(colors, 2, 2, 38, getHeight() - 7,
+ new TAction() {
+ // When the user presses Enter
+ public void DO() {
+ refreshFromTheme(colorNames.getSelected());
+ }
+ },
+ new TAction() {
+ // When the user navigates with keyboard
+ public void DO() {
+ refreshFromTheme(colorNames.getSelected());
+ }
+ },
+ new TAction() {
+ // When the user navigates with keyboard
+ public void DO() {
+ refreshFromTheme(colorNames.getSelected());
+ }
+ }
+ );
+ foreground = new ForegroundPicker(this, 42, 1, 14, 6);
+ background = new BackgroundPicker(this, 42, 7, 14, 4);
+ refreshFromTheme(colors.get(0));
+ colorNames.setSelectedIndex(0);
+
+ addButton(i18n.getString("okButton"), getWidth() - 37, getHeight() - 4,
+ new TAction() {
+ public void DO() {
+ ColorTheme global = getTheme();
+ List<String> colors = editTheme.getColorNames();
+ for (String key: colors) {
+ CellAttributes attr = new CellAttributes();
+ attr.setTo(editTheme.getColor(key));
+ global.setColor(key, attr);
+ }
+ getApplication().closeWindow(TEditColorThemeWindow.this);
+ }
+ }
+ );
+
+ addButton(i18n.getString("cancelButton"), getWidth() - 25,
+ getHeight() - 4,
+ new TAction() {
+ public void DO() {
+ getApplication().closeWindow(TEditColorThemeWindow.this);
+ }
+ }
+ );
+
+ // Default to the color list
+ activate(colorNames);
+
+ // Add shortcut text
+ newStatusBar(i18n.getString("statusBar"));
+ }
+
+ // ------------------------------------------------------------------------
+ // Event handlers ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Handle keystrokes.
+ *
+ * @param keypress keystroke event
+ */
+ @Override
+ public void onKeypress(final TKeypressEvent keypress) {
+ // Escape - behave like cancel
+ if (keypress.equals(kbEsc)) {
+ getApplication().closeWindow(this);
+ return;
+ }
+
+ // Pass to my parent
+ super.onKeypress(keypress);
+ }
+
+ // ------------------------------------------------------------------------
+ // TWindow ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Draw me on screen.
+ */
+ @Override
+ public void draw() {
+ super.draw();
+ CellAttributes attr = new CellAttributes();
+
+ // Draw the label on colorNames
+ attr.setTo(getTheme().getColor("twindow.background.modal"));
+ if (colorNames.isActive()) {
+ attr.setForeColor(getTheme().getColor("tlabel").getForeColor());
+ attr.setBold(getTheme().getColor("tlabel").isBold());
+ }
+ putStringXY(3, 2, i18n.getString("colorName"), attr);
+
+ // Draw the sample text box
+ attr.reset();
+ attr.setForeColor(foreground.color);
+ attr.setBold(foreground.bold);
+ attr.setBackColor(background.color);
+ putStringXY(getWidth() - 17, getHeight() - 6,
+ i18n.getString("textTextText"), attr);
+ putStringXY(getWidth() - 17, getHeight() - 5,
+ i18n.getString("textTextText"), attr);
+ }
+
+ // ------------------------------------------------------------------------
+ // TEditColorThemeWindow --------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Set various widgets/values to the editing theme color.
+ *
+ * @param colorName name of color from theme
+ */
+ private void refreshFromTheme(final String colorName) {
+ CellAttributes attr = editTheme.getColor(colorName);
+ foreground.color = attr.getForeColor();
+ foreground.bold = attr.isBold();
+ background.color = attr.getBackColor();
+ }
+
+ /**
+ * Examines foreground, background, and colorNames and sets the color in
+ * editTheme.
+ */
+ private void saveToEditTheme() {
+ String colorName = colorNames.getSelected();
+ if (colorName == null) {
+ return;
+ }
+ CellAttributes attr = editTheme.getColor(colorName);
+ attr.setForeColor(foreground.color);
+ attr.setBold(foreground.bold);
+ attr.setBackColor(background.color);
+ editTheme.setColor(colorName, attr);
+ }
+
+ }
--- /dev/null
+ foregroundLabel=\ Foreground\
+ backgroundLabel=\ Background\
+ windowTitle=Colors
+ okButton=\ \ &OK\ \
+ cancelButton=&Cancel
+ statusBar=Select Colors
+ colorName=Color Name
+ textTextText=Text Text Text
--- /dev/null
+ /*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+ package jexer;
+
+ import java.io.IOException;
+
+ import jexer.bits.CellAttributes;
+ import jexer.event.TKeypressEvent;
+ import jexer.event.TMouseEvent;
+ import jexer.event.TResizeEvent;
+ import jexer.teditor.Document;
+ import jexer.teditor.Line;
+ import jexer.teditor.Word;
+ import static jexer.TKeypress.*;
+
+ /**
+ * TEditorWidget displays an editable text document. It is unaware of
+ * scrolling behavior, but can respond to mouse and keyboard events.
+ */
+ public class TEditorWidget extends TWidget {
+
+ // ------------------------------------------------------------------------
+ // Constants --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The number of lines to scroll on mouse wheel up/down.
+ */
+ private static final int wheelScrollSize = 3;
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The document being edited.
+ */
+ private Document document;
+
+ /**
+ * The default color for the TEditor class.
+ */
+ private CellAttributes defaultColor = null;
+
+ /**
+ * The topmost line number in the visible area. 0-based.
+ */
+ private int topLine = 0;
+
+ /**
+ * The leftmost column number in the visible area. 0-based.
+ */
+ private int leftColumn = 0;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param text text on the screen
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of text area
+ * @param height height of text area
+ */
+ public TEditorWidget(final TWidget parent, final String text, final int x,
+ final int y, final int width, final int height) {
+
+ // Set parent and window
+ super(parent, x, y, width, height);
+
+ setCursorVisible(true);
+
+ defaultColor = getTheme().getColor("teditor");
+ document = new Document(text, defaultColor);
+ }
+
+ // ------------------------------------------------------------------------
+ // TWidget ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Draw the text box.
+ */
+ @Override
+ public void draw() {
+ for (int i = 0; i < getHeight(); i++) {
+ // Background line
+ getScreen().hLineXY(0, i, getWidth(), ' ', defaultColor);
+
+ // Now draw document's line
+ if (topLine + i < document.getLineCount()) {
+ Line line = document.getLine(topLine + i);
+ int x = 0;
+ for (Word word: line.getWords()) {
+ // For now, we are cheating: draw outside the left region
+ // if needed and let screen do the clipping.
+ getScreen().putStringXY(x - leftColumn, i, word.getText(),
+ word.getColor());
+ x += word.getDisplayLength();
+ if (x - leftColumn > getWidth()) {
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Handle mouse press events.
+ *
+ * @param mouse mouse button press event
+ */
+ @Override
+ public void onMouseDown(final TMouseEvent mouse) {
+ if (mouse.isMouseWheelUp()) {
+ for (int i = 0; i < wheelScrollSize; i++) {
+ if (topLine > 0) {
+ topLine--;
+ alignDocument(false);
+ }
+ }
+ return;
+ }
+ if (mouse.isMouseWheelDown()) {
+ for (int i = 0; i < wheelScrollSize; i++) {
+ if (topLine < document.getLineCount() - 1) {
+ topLine++;
+ alignDocument(true);
+ }
+ }
+ return;
+ }
+
+ if (mouse.isMouse1()) {
+ // Set the row and column
+ int newLine = topLine + mouse.getY();
+ int newX = leftColumn + mouse.getX();
+ if (newLine > document.getLineCount() - 1) {
+ // Go to the end
+ document.setLineNumber(document.getLineCount() - 1);
+ document.end();
+ if (newLine > document.getLineCount() - 1) {
+ setCursorY(document.getLineCount() - 1 - topLine);
+ } else {
+ setCursorY(mouse.getY());
+ }
+ alignCursor();
+ return;
+ }
+
+ document.setLineNumber(newLine);
+ setCursorY(mouse.getY());
+ if (newX >= document.getCurrentLine().getDisplayLength()) {
+ document.end();
+ alignCursor();
+ } else {
+ document.setCursor(newX);
+ setCursorX(mouse.getX());
+ }
+ return;
+ }
+
+ // Pass to children
+ super.onMouseDown(mouse);
+ }
+
+ /**
+ * Handle keystrokes.
+ *
+ * @param keypress keystroke event
+ */
+ @Override
+ public void onKeypress(final TKeypressEvent keypress) {
+ if (keypress.equals(kbLeft)) {
+ document.left();
+ alignTopLine(false);
+ } else if (keypress.equals(kbRight)) {
+ document.right();
+ alignTopLine(true);
+ } else if (keypress.equals(kbAltLeft)
+ || keypress.equals(kbCtrlLeft)
+ ) {
+ document.backwardsWord();
+ alignTopLine(false);
+ } else if (keypress.equals(kbAltRight)
+ || keypress.equals(kbCtrlRight)
+ ) {
+ document.forwardsWord();
+ alignTopLine(true);
+ } else if (keypress.equals(kbUp)) {
+ document.up();
+ alignTopLine(false);
+ } else if (keypress.equals(kbDown)) {
+ document.down();
+ alignTopLine(true);
+ } else if (keypress.equals(kbPgUp)) {
+ document.up(getHeight() - 1);
+ alignTopLine(false);
+ } else if (keypress.equals(kbPgDn)) {
+ document.down(getHeight() - 1);
+ alignTopLine(true);
+ } else if (keypress.equals(kbHome)) {
+ if (document.home()) {
+ leftColumn = 0;
+ if (leftColumn < 0) {
+ leftColumn = 0;
+ }
+ setCursorX(0);
+ }
+ } else if (keypress.equals(kbEnd)) {
+ if (document.end()) {
+ alignCursor();
+ }
+ } else if (keypress.equals(kbCtrlHome)) {
+ document.setLineNumber(0);
+ document.home();
+ topLine = 0;
+ leftColumn = 0;
+ setCursorX(0);
+ setCursorY(0);
+ } else if (keypress.equals(kbCtrlEnd)) {
+ document.setLineNumber(document.getLineCount() - 1);
+ document.end();
+ alignTopLine(false);
+ } else if (keypress.equals(kbIns)) {
+ document.setOverwrite(!document.getOverwrite());
+ } else if (keypress.equals(kbDel)) {
+ document.del();
+ alignCursor();
+ } else if (keypress.equals(kbBackspace)
+ || keypress.equals(kbBackspaceDel)
+ ) {
+ document.backspace();
+ alignTopLine(false);
+ } else if (keypress.equals(kbTab)) {
+ // TODO: tab character. For now just add spaces until we hit
+ // modulo 8.
+ for (int i = document.getCursor(); (i + 1) % 8 != 0; i++) {
+ document.addChar(' ');
+ }
+ alignCursor();
+ } else if (keypress.equals(kbEnter)) {
+ document.enter();
+ alignTopLine(true);
+ } else if (!keypress.getKey().isFnKey()
+ && !keypress.getKey().isAlt()
+ && !keypress.getKey().isCtrl()
+ ) {
+ // Plain old keystroke, process it
+ document.addChar(keypress.getKey().getChar());
+ alignCursor();
+ } else {
+ // Pass other keys (tab etc.) on to TWidget
+ super.onKeypress(keypress);
+ }
+ }
+
+ /**
+ * Method that subclasses can override to handle window/screen resize
+ * events.
+ *
+ * @param resize resize event
+ */
+ @Override
+ public void onResize(final TResizeEvent resize) {
+ // Change my width/height, and pull the cursor in as needed.
+ if (resize.getType() == TResizeEvent.Type.WIDGET) {
+ setWidth(resize.getWidth());
+ setHeight(resize.getHeight());
+ // See if the cursor is now outside the window, and if so move
+ // things.
+ if (getCursorX() >= getWidth()) {
+ leftColumn += getCursorX() - (getWidth() - 1);
+ setCursorX(getWidth() - 1);
+ }
+ if (getCursorY() >= getHeight()) {
+ topLine += getCursorY() - (getHeight() - 1);
+ setCursorY(getHeight() - 1);
+ }
+ } else {
+ // Let superclass handle it
+ super.onResize(resize);
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // TEditorWidget ----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Align visible area with document current line.
+ *
+ * @param topLineIsTop if true, make the top visible line the document
+ * current line if it was off-screen. If false, make the bottom visible
+ * line the document current line.
+ */
+ private void alignTopLine(final boolean topLineIsTop) {
+ int line = document.getLineNumber();
+
+ if ((line < topLine) || (line > topLine + getHeight() - 1)) {
+ // Need to move topLine to bring document back into view.
+ if (topLineIsTop) {
+ topLine = line - (getHeight() - 1);
+ if (topLine < 0) {
+ topLine = 0;
+ }
+ assert (topLine >= 0);
+ } else {
+ topLine = line;
+ assert (topLine >= 0);
+ }
+ }
+
+ /*
+ System.err.println("line " + line + " topLine " + topLine);
+ */
+
+ // Document is in view, let's set cursorY
+ assert (line >= topLine);
+ setCursorY(line - topLine);
+ alignCursor();
+ }
+
+ /**
+ * Align document current line with visible area.
+ *
+ * @param topLineIsTop if true, make the top visible line the document
+ * current line if it was off-screen. If false, make the bottom visible
+ * line the document current line.
+ */
+ private void alignDocument(final boolean topLineIsTop) {
+ int line = document.getLineNumber();
+ int cursor = document.getCursor();
+
+ if ((line < topLine) || (line > topLine + getHeight() - 1)) {
+ // Need to move document to ensure it fits view.
+ if (topLineIsTop) {
+ document.setLineNumber(topLine);
+ } else {
+ document.setLineNumber(topLine + (getHeight() - 1));
+ }
+ if (cursor < document.getCurrentLine().getDisplayLength()) {
+ document.setCursor(cursor);
+ }
+ }
+
+ /*
+ System.err.println("getLineNumber() " + document.getLineNumber() +
+ " topLine " + topLine);
+ */
+
+ // Document is in view, let's set cursorY
+ setCursorY(document.getLineNumber() - topLine);
+ alignCursor();
+ }
+
+ /**
+ * Align visible cursor with document cursor.
+ */
+ private void alignCursor() {
+ int width = getWidth();
+
+ int desiredX = document.getCursor() - leftColumn;
+ if (desiredX < 0) {
+ // We need to push the screen to the left.
+ leftColumn = document.getCursor();
+ } else if (desiredX > width - 1) {
+ // We need to push the screen to the right.
+ leftColumn = document.getCursor() - (width - 1);
+ }
+
+ /*
+ System.err.println("document cursor " + document.getCursor() +
+ " leftColumn " + leftColumn);
+ */
+
+
+ setCursorX(document.getCursor() - leftColumn);
+ }
+
+ /**
+ * Get the number of lines in the underlying Document.
+ *
+ * @return the number of lines
+ */
+ public int getLineCount() {
+ return document.getLineCount();
+ }
+
+ /**
+ * Get the current visible top row number. 1-based.
+ *
+ * @return the visible top row number. Row 1 is the first row.
+ */
+ public int getVisibleRowNumber() {
+ return topLine + 1;
+ }
+
+ /**
+ * Set the current visible row number. 1-based.
+ *
+ * @param row the new visible row number. Row 1 is the first row.
+ */
+ public void setVisibleRowNumber(final int row) {
+ assert (row > 0);
+ if ((row > 0) && (row < document.getLineCount())) {
+ topLine = row - 1;
+ alignDocument(true);
+ }
+ }
+
+ /**
+ * Get the current editing row number. 1-based.
+ *
+ * @return the editing row number. Row 1 is the first row.
+ */
+ public int getEditingRowNumber() {
+ return document.getLineNumber() + 1;
+ }
+
+ /**
+ * Set the current editing row number. 1-based.
+ *
+ * @param row the new editing row number. Row 1 is the first row.
+ */
+ public void setEditingRowNumber(final int row) {
+ assert (row > 0);
+ if ((row > 0) && (row < document.getLineCount())) {
+ document.setLineNumber(row - 1);
+ alignTopLine(true);
+ }
+ }
+
+ /**
+ * Set the current visible column number. 1-based.
+ *
+ * @return the visible column number. Column 1 is the first column.
+ */
+ public int getVisibleColumnNumber() {
+ return leftColumn + 1;
+ }
+
+ /**
+ * Set the current visible column number. 1-based.
+ *
+ * @param column the new visible column number. Column 1 is the first
+ * column.
+ */
+ public void setVisibleColumnNumber(final int column) {
+ assert (column > 0);
+ if ((column > 0) && (column < document.getLineLengthMax())) {
+ leftColumn = column - 1;
+ alignDocument(true);
+ }
+ }
+
+ /**
+ * Get the current editing column number. 1-based.
+ *
+ * @return the editing column number. Column 1 is the first column.
+ */
+ public int getEditingColumnNumber() {
+ return document.getCursor() + 1;
+ }
+
+ /**
+ * Set the current editing column number. 1-based.
+ *
+ * @param column the new editing column number. Column 1 is the first
+ * column.
+ */
+ public void setEditingColumnNumber(final int column) {
+ if ((column > 0) && (column < document.getLineLength())) {
+ document.setCursor(column - 1);
+ alignCursor();
+ }
+ }
+
+ /**
+ * Get the maximum possible row number. 1-based.
+ *
+ * @return the maximum row number. Row 1 is the first row.
+ */
+ public int getMaximumRowNumber() {
+ return document.getLineCount() + 1;
+ }
+
+ /**
+ * Get the maximum possible column number. 1-based.
+ *
+ * @return the maximum column number. Column 1 is the first column.
+ */
+ public int getMaximumColumnNumber() {
+ return document.getLineLengthMax() + 1;
+ }
+
+ /**
+ * Get the dirty value.
+ *
+ * @return true if the buffer is dirty
+ */
+ public boolean isDirty() {
+ return document.isDirty();
+ }
+
+ /**
+ * Save contents to file.
+ *
+ * @param filename file to save to
+ * @throws IOException if a java.io operation throws
+ */
+ public void saveToFilename(final String filename) throws IOException {
+ document.saveToFilename(filename);
+ }
+
+ }
--- /dev/null
+ /*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+ package jexer;
+
+ import java.io.File;
+ import java.io.IOException;
+ import java.text.MessageFormat;
+ import java.util.ResourceBundle;
+ import java.util.Scanner;
+
+ import jexer.TApplication;
+ import jexer.TEditorWidget;
+ import jexer.THScroller;
+ import jexer.TScrollableWindow;
+ import jexer.TVScroller;
+ import jexer.TWidget;
+ import jexer.bits.CellAttributes;
+ import jexer.bits.GraphicsChars;
+ import jexer.event.TCommandEvent;
+ import jexer.event.TKeypressEvent;
+ import jexer.event.TMouseEvent;
+ import jexer.event.TResizeEvent;
+ import static jexer.TCommand.*;
+ import static jexer.TKeypress.*;
+
+ /**
+ * TEditorWindow is a basic text file editor.
+ */
+ public class TEditorWindow extends TScrollableWindow {
+
+ /**
+ * Translated strings.
+ */
+ private static final ResourceBundle i18n = ResourceBundle.getBundle(TEditorWindow.class.getName());
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Hang onto my TEditor so I can resize it with the window.
+ */
+ private TEditorWidget editField;
+
+ /**
+ * The fully-qualified name of the file being edited.
+ */
+ private String filename = "";
+
+ /**
+ * If true, hide the mouse after typing a keystroke.
+ */
+ private boolean hideMouseWhenTyping = true;
+
+ /**
+ * If true, the mouse should not be displayed because a keystroke was
+ * typed.
+ */
+ private boolean typingHidMouse = false;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor sets window title.
+ *
+ * @param parent the main application
+ * @param title the window title
+ */
+ public TEditorWindow(final TApplication parent, final String title) {
+
+ super(parent, title, 0, 0, parent.getScreen().getWidth(),
+ parent.getDesktopBottom() - parent.getDesktopTop(), RESIZABLE);
+
+ editField = addEditor("", 0, 0, getWidth() - 2, getHeight() - 2);
+ setupAfterEditor();
+ }
+
+ /**
+ * Public constructor sets window title and contents.
+ *
+ * @param parent the main application
+ * @param title the window title, usually a filename
+ * @param contents the data for the editing window, usually the file data
+ */
+ public TEditorWindow(final TApplication parent, final String title,
+ final String contents) {
+
+ super(parent, title, 0, 0, parent.getScreen().getWidth(),
+ parent.getDesktopBottom() - parent.getDesktopTop(), RESIZABLE);
+
+ filename = title;
+ editField = addEditor(contents, 0, 0, getWidth() - 2, getHeight() - 2);
+ setupAfterEditor();
+ }
+
+ /**
+ * Public constructor opens a file.
+ *
+ * @param parent the main application
+ * @param file the file to open
+ * @throws IOException if a java.io operation throws
+ */
+ public TEditorWindow(final TApplication parent,
+ final File file) throws IOException {
+
+ super(parent, file.getName(), 0, 0, parent.getScreen().getWidth(),
+ parent.getDesktopBottom() - parent.getDesktopTop(), RESIZABLE);
+
+ filename = file.getName();
+ String contents = readFileData(file);
+ editField = addEditor(contents, 0, 0, getWidth() - 2, getHeight() - 2);
+ setupAfterEditor();
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param parent the main application
+ */
+ public TEditorWindow(final TApplication parent) {
+ this(parent, i18n.getString("newTextDocument"));
+ }
+
+ // ------------------------------------------------------------------------
+ // 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);
+ }
+ }
+
+ /**
+ * Handle mouse press events.
+ *
+ * @param mouse mouse button press event
+ */
+ @Override
+ public void onMouseDown(final TMouseEvent mouse) {
+ // Use TWidget's code to pass the event to the children.
+ super.onMouseDown(mouse);
+
+ if (hideMouseWhenTyping) {
+ typingHidMouse = false;
+ }
+
+ if (mouseOnEditor(mouse)) {
+ // The editor might have changed, update the scollbars.
+ setBottomValue(editField.getMaximumRowNumber());
+ setVerticalValue(editField.getVisibleRowNumber());
+ setRightValue(editField.getMaximumColumnNumber());
+ setHorizontalValue(editField.getEditingColumnNumber());
+ } else {
+ if (mouse.isMouseWheelUp() || mouse.isMouseWheelDown()) {
+ // Vertical scrollbar actions
+ editField.setVisibleRowNumber(getVerticalValue());
+ }
+ }
+ }
+
+ /**
+ * Handle mouse release events.
+ *
+ * @param mouse mouse button release event
+ */
+ @Override
+ public void onMouseUp(final TMouseEvent mouse) {
+ // Use TWidget's code to pass the event to the children.
+ super.onMouseUp(mouse);
+
+ if (hideMouseWhenTyping) {
+ typingHidMouse = false;
+ }
+
+ if (mouse.isMouse1() && mouseOnVerticalScroller(mouse)) {
+ // Clicked on vertical scrollbar
+ editField.setVisibleRowNumber(getVerticalValue());
+ }
+ if (mouse.isMouse1() && mouseOnHorizontalScroller(mouse)) {
+ // Clicked on horizontal scrollbar
+ editField.setVisibleColumnNumber(getHorizontalValue());
+ setHorizontalValue(editField.getVisibleColumnNumber());
+ }
+ }
+
+ /**
+ * Method that subclasses can override to handle mouse movements.
+ *
+ * @param mouse mouse motion event
+ */
+ @Override
+ public void onMouseMotion(final TMouseEvent mouse) {
+ // Use TWidget's code to pass the event to the children.
+ super.onMouseMotion(mouse);
+
+ if (hideMouseWhenTyping) {
+ typingHidMouse = false;
+ }
+
+ if (mouseOnEditor(mouse) && mouse.isMouse1()) {
+ // The editor might have changed, update the scollbars.
+ setBottomValue(editField.getMaximumRowNumber());
+ setVerticalValue(editField.getVisibleRowNumber());
+ setRightValue(editField.getMaximumColumnNumber());
+ setHorizontalValue(editField.getEditingColumnNumber());
+ } else {
+ if (mouse.isMouse1() && mouseOnVerticalScroller(mouse)) {
+ // Clicked/dragged on vertical scrollbar
+ editField.setVisibleRowNumber(getVerticalValue());
+ }
+ if (mouse.isMouse1() && mouseOnHorizontalScroller(mouse)) {
+ // Clicked/dragged on horizontal scrollbar
+ editField.setVisibleColumnNumber(getHorizontalValue());
+ setHorizontalValue(editField.getVisibleColumnNumber());
+ }
+ }
+
+ }
+
+ /**
+ * Handle keystrokes.
+ *
+ * @param keypress keystroke event
+ */
+ @Override
+ public void onKeypress(final TKeypressEvent keypress) {
+ if (hideMouseWhenTyping) {
+ typingHidMouse = true;
+ }
+
+ // Use TWidget's code to pass the event to the children.
+ super.onKeypress(keypress);
+
+ // The editor might have changed, update the scollbars.
+ setBottomValue(editField.getMaximumRowNumber());
+ setVerticalValue(editField.getVisibleRowNumber());
+ setRightValue(editField.getMaximumColumnNumber());
+ setHorizontalValue(editField.getEditingColumnNumber());
+ }
+
+ /**
+ * Handle window/screen resize events.
+ *
+ * @param event resize event
+ */
+ @Override
+ public void onResize(final TResizeEvent event) {
+ if (event.getType() == TResizeEvent.Type.WIDGET) {
+ // Resize the text field
+ TResizeEvent editSize = new TResizeEvent(TResizeEvent.Type.WIDGET,
+ event.getWidth() - 2, event.getHeight() - 2);
+ editField.onResize(editSize);
+
+ // Have TScrollableWindow handle the scrollbars
+ super.onResize(event);
+ return;
+ }
+
+ // Pass to children instead
+ for (TWidget widget: getChildren()) {
+ widget.onResize(event);
+ }
+ }
+
+ /**
+ * Method that subclasses can override to handle posted command events.
+ *
+ * @param command command event
+ */
+ @Override
+ public void onCommand(final TCommandEvent command) {
+ if (command.equals(cmOpen)) {
+ try {
+ String filename = fileOpenBox(".");
+ if (filename != null) {
+ try {
+ String contents = readFileData(filename);
+ new TEditorWindow(getApplication(), filename, contents);
+ } catch (IOException e) {
+ messageBox(i18n.getString("errorDialogTitle"),
+ MessageFormat.format(i18n.
+ getString("errorReadingFile"), e.getMessage()));
+ }
+ }
+ } catch (IOException e) {
+ messageBox(i18n.getString("errorDialogTitle"),
+ MessageFormat.format(i18n.
+ getString("errorOpeningFileDialog"), e.getMessage()));
+ }
+ return;
+ }
+
+ if (command.equals(cmSave)) {
+ if (filename.length() > 0) {
+ try {
+ editField.saveToFilename(filename);
+ } catch (IOException e) {
+ messageBox(i18n.getString("errorDialogTitle"),
+ MessageFormat.format(i18n.
+ getString("errorSavingFile"), e.getMessage()));
+ }
+ }
+ return;
+ }
+
+ // Didn't handle it, let children get it instead
+ super.onCommand(command);
+ }
+
+ /**
+ * Returns true if this window does not want the application-wide mouse
+ * cursor drawn over it.
+ *
+ * @return true if this window does not want the application-wide mouse
+ * cursor drawn over it
+ */
+ @Override
+ public boolean hasHiddenMouse() {
+ return (super.hasHiddenMouse() || typingHidMouse);
+ }
+
+ // ------------------------------------------------------------------------
+ // TEditorWindow ----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Setup other fields after the editor is created.
+ */
+ private void setupAfterEditor() {
+ hScroller = new THScroller(this, 17, getHeight() - 2, getWidth() - 20);
+ vScroller = new TVScroller(this, getWidth() - 2, 0, getHeight() - 2);
+ setMinimumWindowWidth(25);
+ setMinimumWindowHeight(10);
+ setTopValue(1);
+ setBottomValue(editField.getMaximumRowNumber());
+ setLeftValue(1);
+ setRightValue(editField.getMaximumColumnNumber());
+
+ statusBar = newStatusBar(i18n.getString("statusBar"));
+ statusBar.addShortcutKeypress(kbF1, cmHelp,
+ i18n.getString("statusBarHelp"));
+ statusBar.addShortcutKeypress(kbF2, cmSave,
+ i18n.getString("statusBarSave"));
+ statusBar.addShortcutKeypress(kbF3, cmOpen,
+ i18n.getString("statusBarOpen"));
+ statusBar.addShortcutKeypress(kbF10, cmMenu,
+ i18n.getString("statusBarMenu"));
+
+ // Hide mouse when typing option
+ if (System.getProperty("jexer.TEditor.hideMouseWhenTyping",
+ "true").equals("false")) {
+
+ hideMouseWhenTyping = false;
+ }
+ }
+
+ /**
+ * Read file data into a string.
+ *
+ * @param file the file to open
+ * @return the file contents
+ * @throws IOException if a java.io operation throws
+ */
+ private String readFileData(final File file) throws IOException {
+ StringBuilder fileContents = new StringBuilder();
+ Scanner scanner = new Scanner(file);
+ String EOL = System.getProperty("line.separator");
+
+ try {
+ while (scanner.hasNextLine()) {
+ fileContents.append(scanner.nextLine() + EOL);
+ }
+ return fileContents.toString();
+ } finally {
+ scanner.close();
+ }
+ }
+
+ /**
+ * Read file data into a string.
+ *
+ * @param filename the file to open
+ * @return the file contents
+ * @throws IOException if a java.io operation throws
+ */
+ private String readFileData(final String filename) throws IOException {
+ return readFileData(new File(filename));
+ }
+
+ /**
+ * Check if a mouse press/release/motion event coordinate is over the
+ * editor.
+ *
+ * @param mouse a mouse-based event
+ * @return whether or not the mouse is on the editor
+ */
+ private boolean mouseOnEditor(final TMouseEvent mouse) {
+ if ((mouse.getAbsoluteX() >= getAbsoluteX() + 1)
+ && (mouse.getAbsoluteX() < getAbsoluteX() + getWidth() - 1)
+ && (mouse.getAbsoluteY() >= getAbsoluteY() + 1)
+ && (mouse.getAbsoluteY() < getAbsoluteY() + getHeight() - 1)
+ ) {
+ return true;
+ }
+ return false;
+ }
+
+ }
--- /dev/null
+ statusBar=Editor
+ statusBarHelp=Help
+ statusBarSave=Save
+ statusBarOpen=Open
+ statusBarMenu=Menu
+ newTextDocument=New Text Document
+ errorDialogTitle=Error
+ errorReadingFile=Error reading file: {0}
+ errorOpeningFileDialog=Error opening file dialog: {0}
+ errorSavingFile=Error saving file: {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;
+
+ import java.io.FileWriter;
+ import java.io.IOException;
+ import java.io.PrintWriter;
+ import java.text.MessageFormat;
+ import java.util.ArrayList;
+ import java.util.Date;
+ import java.util.ResourceBundle;
+
+ import jexer.bits.CellAttributes;
+
+ /**
+ * TExceptionDialog displays an exception and its stack trace to the user,
+ * and provides a means to save a troubleshooting report for support.
+ */
+ public class TExceptionDialog extends TWindow {
+
+ /**
+ * Translated strings.
+ */
+ private static final ResourceBundle i18n = ResourceBundle.getBundle(TExceptionDialog.class.getName());
+
+ // ------------------------------------------------------------------------
+ // Constants --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The exception. We will actually make it Throwable, for the unlikely
+ * event we catch an Error rather than an Exception.
+ */
+ private Throwable exception;
+
+ /**
+ * The exception's stack trace.
+ */
+ private TList stackTrace;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param application TApplication that manages this window
+ * @param exception the exception to display
+ */
+ public TExceptionDialog(final TApplication application,
+ final Throwable exception) {
+
+ super(application, i18n.getString("windowTitle"),
+ 1, 1, 70, 20, CENTERED | MODAL);
+
+ this.exception = exception;
+
+ addLabel(i18n.getString("captionLine1"), 1, 1,
+ "twindow.background.modal");
+ addLabel(i18n.getString("captionLine2"), 1, 2,
+ "twindow.background.modal");
+ addLabel(i18n.getString("captionLine3"), 1, 3,
+ "twindow.background.modal");
+ addLabel(i18n.getString("captionLine4"), 1, 4,
+ "twindow.background.modal");
+
+ addLabel(MessageFormat.format(i18n.getString("exceptionString"),
+ exception.getClass().getName(), exception.getMessage()),
+ 2, 6, "ttext", false);
+
+ ArrayList<String> stackTraceStrings = new ArrayList<String>();
+ 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);
+
+ // Buttons
+ addButton(i18n.getString("saveButton"), 19, getHeight() - 4,
+ new TAction() {
+ public void DO() {
+ saveToFile();
+ }
+ });
+
+ TButton closeButton = addButton(i18n.getString("closeButton"),
+ 35, getHeight() - 4,
+ new TAction() {
+ public void DO() {
+ // Don't do anything, just close the window.
+ TExceptionDialog.this.close();
+ }
+ });
+
+ // Save this for last: make the close button default action.
+ activate(closeButton);
+ }
+
+ // ------------------------------------------------------------------------
+ // TWindow ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Draw the exception message background.
+ */
+ @Override
+ public void draw() {
+ // Draw window and border.
+ super.draw();
+
+ CellAttributes boxColor = getTheme().getColor("ttext");
+ hLineXY(3, 7, getWidth() - 6, ' ', boxColor);
+ }
+
+ // ------------------------------------------------------------------------
+ // TExceptionDialog -------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Save a troubleshooting report to file. Note that we do NOT translate
+ * the strings within the error report.
+ */
+ private void saveToFile() {
+ // Prompt for filename.
+ PrintWriter writer = null;
+ try {
+ String filename = fileSaveBox(".");
+ if (filename == null) {
+ // User cancelled, bail out.
+ return;
+ }
+ writer = new PrintWriter(new FileWriter(filename));
+ writer.write("Date: " + new Date(System.currentTimeMillis())
+ + "\n");
+
+ // System properties
+ writer.write("System properties:\n");
+ writer.write("-----------------------------------\n");
+ System.getProperties().store(writer, null);
+ writer.write("-----------------------------------\n");
+ writer.write("\n");
+
+ // The exception we caught
+ writer.write("Caught exception:\n");
+ writer.write("-----------------------------------\n");
+ exception.printStackTrace(writer);
+ writer.write("-----------------------------------\n");
+ writer.write("\n");
+ // The exception's cause, if it was set
+ if (exception.getCause() != null) {
+ writer.write("Caught exception's cause:\n");
+ writer.write("-----------------------------------\n");
+ exception.getCause().printStackTrace(writer);
+ writer.write("-----------------------------------\n");
+ }
+ writer.write("\n");
+
+ // The UI stack trace
+ writer.write("UI stack trace:\n");
+ writer.write("-----------------------------------\n");
+ (new Throwable("UI Thread")).printStackTrace(writer);
+ writer.write("-----------------------------------\n");
+ writer.write("\n");
+ writer.close();
+ } catch (IOException e) {
+ messageBox(i18n.getString("errorDialogTitle"),
+ MessageFormat.format(i18n.
+ getString("errorSavingFile"), e.getMessage()));
+ } finally {
+ if (writer != null) {
+ writer.close();
+ writer = null;
+ }
+ }
+ }
+ }
--- /dev/null
+ 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.
+
+ exceptionString={0}: {1}
+
+ saveButton=&Save Report
+ closeButton=\ \ \ &Close\ \ \
+
+ errorDialogTitle=Error
+ errorSavingFile=Error saving file: {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;
+
+ import jexer.bits.CellAttributes;
+ import jexer.bits.GraphicsChars;
+ import jexer.bits.StringUtils;
+ import jexer.event.TKeypressEvent;
+ import jexer.event.TMouseEvent;
+ import static jexer.TKeypress.*;
+
+ /**
+ * TField implements an editable text field.
+ */
+ public class TField extends TWidget {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Background character for unfilled-in text.
+ */
+ protected int backgroundChar = GraphicsChars.HATCH;
+
+ /**
+ * Field text.
+ */
+ protected String text = "";
+
+ /**
+ * If true, only allow enough characters that will fit in the width. If
+ * false, allow the field to scroll to the right.
+ */
+ protected boolean fixed = false;
+
+ /**
+ * Current editing position within text.
+ */
+ protected int position = 0;
+
+ /**
+ * Current editing position screen column number.
+ */
+ protected int screenPosition = 0;
+
+ /**
+ * Beginning of visible portion.
+ */
+ protected int windowStart = 0;
+
+ /**
+ * If true, new characters are inserted at position.
+ */
+ protected boolean insertMode = true;
+
+ /**
+ * Remember mouse state.
+ */
+ protected TMouseEvent mouse;
+
+ /**
+ * The action to perform when the user presses enter.
+ */
+ protected TAction enterAction;
+
+ /**
+ * The action to perform when the text is updated.
+ */
+ protected TAction updateAction;
+
+ /**
+ * The color to use when this field is active.
+ */
+ private String activeColorKey = "tfield.active";
+
+ /**
+ * The color to use when this field is not active.
+ */
+ private String inactiveColorKey = "tfield.inactive";
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width visible text width
+ * @param fixed if true, the text cannot exceed the display width
+ */
+ public TField(final TWidget parent, final int x, final int y,
+ final int width, final boolean fixed) {
+
+ this(parent, x, y, width, fixed, "", null, null);
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width visible text width
+ * @param fixed if true, the text cannot exceed the display width
+ * @param text initial text, default is empty string
+ */
+ public TField(final TWidget parent, final int x, final int y,
+ final int width, final boolean fixed, final String text) {
+
+ this(parent, x, y, width, fixed, text, null, null);
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width visible text width
+ * @param fixed if true, the text cannot exceed the display width
+ * @param text initial text, default is empty string
+ * @param enterAction function to call when enter key is pressed
+ * @param updateAction function to call when the text is updated
+ */
+ public TField(final TWidget parent, final int x, final int y,
+ final int width, final boolean fixed, final String text,
+ final TAction enterAction, final TAction updateAction) {
+
+ // Set parent and window
+ super(parent, x, y, width, 1);
+
+ setCursorVisible(true);
+ this.fixed = fixed;
+ this.text = text;
+ this.enterAction = enterAction;
+ this.updateAction = updateAction;
+ }
+
+ // ------------------------------------------------------------------------
+ // Event handlers ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Returns true if the mouse is currently on the field.
+ *
+ * @return if true the mouse is currently on the field
+ */
+ protected boolean mouseOnField() {
+ int rightEdge = getWidth() - 1;
+ if ((mouse != null)
+ && (mouse.getY() == 0)
+ && (mouse.getX() >= 0)
+ && (mouse.getX() <= rightEdge)
+ ) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Handle mouse button presses.
+ *
+ * @param mouse mouse button event
+ */
+ @Override
+ public void onMouseDown(final TMouseEvent mouse) {
+ this.mouse = mouse;
+
+ if ((mouseOnField()) && (mouse.isMouse1())) {
+ // Move cursor
+ int deltaX = mouse.getX() - getCursorX();
+ screenPosition += deltaX;
+ if (screenPosition > StringUtils.width(text)) {
+ screenPosition = StringUtils.width(text);
+ }
+ position = screenToTextPosition(screenPosition);
+ updateCursor();
+ return;
+ }
+ }
+
+ /**
+ * Handle keystrokes.
+ *
+ * @param keypress keystroke event
+ */
+ @Override
+ public void onKeypress(final TKeypressEvent keypress) {
+
+ if (keypress.equals(kbLeft)) {
+ if (position > 0) {
+ screenPosition -= StringUtils.width(text.codePointBefore(position));
+ position -= Character.charCount(text.codePointBefore(position));
+ }
+ if (fixed == false) {
+ if ((screenPosition == windowStart) && (windowStart > 0)) {
+ windowStart -= StringUtils.width(text.codePointAt(
+ screenToTextPosition(windowStart)));
+ }
+ }
+ normalizeWindowStart();
+ return;
+ }
+
+ if (keypress.equals(kbRight)) {
+ if (position < text.length()) {
+ 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));
+ }
+ } else {
+ while ((screenPosition - windowStart +
+ StringUtils.width(text.codePointAt(text.length() - 1)))
+ > getWidth()
+ ) {
+ windowStart += StringUtils.width(text.codePointAt(
+ screenToTextPosition(windowStart)));
+ }
+ }
+ }
+ assert (position <= text.length());
+ return;
+ }
+
+ if (keypress.equals(kbEnter)) {
+ dispatch(true);
+ return;
+ }
+
+ if (keypress.equals(kbIns)) {
+ insertMode = !insertMode;
+ return;
+ }
+ if (keypress.equals(kbHome)) {
+ home();
+ return;
+ }
+
+ if (keypress.equals(kbEnd)) {
+ end();
+ return;
+ }
+
+ if (keypress.equals(kbDel)) {
+ if ((text.length() > 0) && (position < text.length())) {
+ text = text.substring(0, position)
+ + text.substring(position + 1);
+ screenPosition = StringUtils.width(text.substring(0, position));
+ }
+ dispatch(false);
+ return;
+ }
+
+ if (keypress.equals(kbBackspace) || keypress.equals(kbBackspaceDel)) {
+ if (position > 0) {
+ position -= Character.charCount(text.codePointBefore(position));
+ text = text.substring(0, position)
+ + text.substring(position + 1);
+ screenPosition = StringUtils.width(text.substring(0, position));
+ }
+ if (fixed == false) {
+ if ((screenPosition >= windowStart)
+ && (windowStart > 0)
+ ) {
+ windowStart -= StringUtils.width(text.codePointAt(
+ screenToTextPosition(windowStart)));
+ }
+ }
+ dispatch(false);
+ normalizeWindowStart();
+ return;
+ }
+
+ if (!keypress.getKey().isFnKey()
+ && !keypress.getKey().isAlt()
+ && !keypress.getKey().isCtrl()
+ ) {
+ // Plain old keystroke, process it
+ if ((position == text.length())
+ && (StringUtils.width(text) < getWidth())) {
+
+ // Append case
+ appendChar(keypress.getKey().getChar());
+ } else if ((position < text.length())
+ && (StringUtils.width(text) < getWidth())) {
+
+ // Overwrite or insert a character
+ if (insertMode == false) {
+ // Replace character
+ text = text.substring(0, position)
+ + codePointString(keypress.getKey().getChar())
+ + text.substring(position + 1);
+ screenPosition += StringUtils.width(text.codePointAt(position));
+ position += Character.charCount(keypress.getKey().getChar());
+ } else {
+ // Insert character
+ insertChar(keypress.getKey().getChar());
+ }
+ } else if ((position < text.length())
+ && (StringUtils.width(text) >= getWidth())) {
+
+ // Multiple cases here
+ if ((fixed == true) && (insertMode == true)) {
+ // Buffer is full, do nothing
+ } else if ((fixed == true) && (insertMode == false)) {
+ // Overwrite the last character, maybe move position
+ text = text.substring(0, position)
+ + codePointString(keypress.getKey().getChar())
+ + text.substring(position + 1);
+ if (screenPosition < getWidth() - 1) {
+ screenPosition += StringUtils.width(text.codePointAt(position));
+ position += Character.charCount(keypress.getKey().getChar());
+ }
+ } else if ((fixed == false) && (insertMode == false)) {
+ // Overwrite the last character, definitely move position
+ text = text.substring(0, position)
+ + codePointString(keypress.getKey().getChar())
+ + text.substring(position + 1);
+ screenPosition += StringUtils.width(text.codePointAt(position));
+ position += Character.charCount(keypress.getKey().getChar());
+ } else {
+ if (position == text.length()) {
+ // Append this character
+ appendChar(keypress.getKey().getChar());
+ } else {
+ // Insert this character
+ insertChar(keypress.getKey().getChar());
+ }
+ }
+ } else {
+ assert (!fixed);
+
+ // Append this character
+ appendChar(keypress.getKey().getChar());
+ }
+ dispatch(false);
+ return;
+ }
+
+ // Pass to parent for the things we don't care about.
+ super.onKeypress(keypress);
+ }
+
+ // ------------------------------------------------------------------------
+ // TWidget ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Override TWidget's height: we can only set height at construction
+ * time.
+ *
+ * @param height new widget height (ignored)
+ */
+ @Override
+ public void setHeight(final int height) {
+ // Do nothing
+ }
+
+ /**
+ * Draw the text field.
+ */
+ @Override
+ public void draw() {
+ CellAttributes fieldColor;
+
+ if (isAbsoluteActive()) {
+ fieldColor = getTheme().getColor(activeColorKey);
+ } else {
+ fieldColor = getTheme().getColor(inactiveColorKey);
+ }
+
+ int end = windowStart + getWidth();
+ if (end > StringUtils.width(text)) {
+ end = StringUtils.width(text);
+ }
+ hLineXY(0, 0, getWidth(), backgroundChar, fieldColor);
+ putStringXY(0, 0, text.substring(screenToTextPosition(windowStart),
+ screenToTextPosition(end)), fieldColor);
+
+ // Fix the cursor, it will be rendered by TApplication.drawAll().
+ updateCursor();
+ }
+
+ // ------------------------------------------------------------------------
+ // TField -----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Convert a char (codepoint) to a string.
+ *
+ * @param ch the char
+ * @return the string
+ */
+ private String codePointString(final int ch) {
+ StringBuilder sb = new StringBuilder(1);
+ sb.append(Character.toChars(ch));
+ assert (Character.charCount(ch) == sb.length());
+ return sb.toString();
+ }
+
+ /**
+ * Get field background character.
+ *
+ * @return background character
+ */
+ public final int getBackgroundChar() {
+ return backgroundChar;
+ }
+
+ /**
+ * Set field background character.
+ *
+ * @param backgroundChar the background character
+ */
+ public void setBackgroundChar(final int backgroundChar) {
+ this.backgroundChar = backgroundChar;
+ }
+
+ /**
+ * Get field text.
+ *
+ * @return field text
+ */
+ public final String getText() {
+ return text;
+ }
+
+ /**
+ * Set field text.
+ *
+ * @param text the new field text
+ */
+ public void setText(final String text) {
+ assert (text != null);
+ this.text = text;
+ position = 0;
+ windowStart = 0;
+ }
+
+ /**
+ * Dispatch to the action function.
+ *
+ * @param enter if true, the user pressed Enter, else this was an update
+ * to the text.
+ */
+ protected void dispatch(final boolean enter) {
+ if (enter) {
+ if (enterAction != null) {
+ enterAction.DO(this);
+ }
+ } else {
+ if (updateAction != null) {
+ updateAction.DO(this);
+ }
+ }
+ }
+
+ /**
+ * Determine string position from screen position.
+ *
+ * @param screenPosition the position on screen
+ * @return the equivalent position in text
+ */
+ protected int screenToTextPosition(final int screenPosition) {
+ if (screenPosition == 0) {
+ return 0;
+ }
+
+ int n = 0;
+ for (int i = 0; i < text.length(); i++) {
+ n += StringUtils.width(text.codePointAt(i));
+ if (n >= screenPosition) {
+ return i + 1;
+ }
+ }
+ // screenPosition exceeds the available text length.
+ throw new IndexOutOfBoundsException("screenPosition " + screenPosition +
+ " exceeds available text length " + text.length());
+ }
+
+ /**
+ * Update the visible cursor position to match the location of position
+ * and windowStart.
+ */
+ protected void updateCursor() {
+ if ((screenPosition > getWidth()) && fixed) {
+ setCursorX(getWidth());
+ } else if ((screenPosition - windowStart >= getWidth()) && !fixed) {
+ setCursorX(getWidth() - 1);
+ } else {
+ setCursorX(screenPosition - windowStart);
+ }
+ }
+
+ /**
+ * Normalize windowStart such that most of the field data if visible.
+ */
+ protected void normalizeWindowStart() {
+ if (fixed) {
+ // windowStart had better be zero, there is nothing to do here.
+ assert (windowStart == 0);
+ return;
+ }
+ windowStart = screenPosition - (getWidth() - 1);
+ if (windowStart < 0) {
+ windowStart = 0;
+ }
+
+ updateCursor();
+ }
+
+ /**
+ * Append char to the end of the field.
+ *
+ * @param ch char to append
+ */
+ protected void appendChar(final int ch) {
+ // Append the LAST character
+ text += codePointString(ch);
+ position += Character.charCount(ch);
+ screenPosition += StringUtils.width(ch);
+
+ assert (position == text.length());
+
+ if (fixed) {
+ if (screenPosition >= getWidth()) {
+ position -= Character.charCount(ch);
+ screenPosition -= StringUtils.width(ch);
+ }
+ } else {
+ if ((screenPosition - windowStart) >= getWidth()) {
+ windowStart++;
+ }
+ }
+ }
+
+ /**
+ * Insert char somewhere in the middle of the field.
+ *
+ * @param ch char to append
+ */
+ protected void insertChar(final int ch) {
+ text = text.substring(0, position) + codePointString(ch)
+ + text.substring(position);
+ position += Character.charCount(ch);
+ screenPosition += StringUtils.width(ch);
+ if ((screenPosition - windowStart) == getWidth()) {
+ assert (!fixed);
+ windowStart++;
+ }
+ }
+
+ /**
+ * Position the cursor at the first column. The field may adjust the
+ * window start to show as much of the field as possible.
+ */
+ public void home() {
+ position = 0;
+ screenPosition = 0;
+ windowStart = 0;
+ }
+
+ /**
+ * Set the editing position to the last filled character. The field may
+ * adjust the window start to show as much of the field as possible.
+ */
+ public void end() {
+ position = text.length();
+ screenPosition = StringUtils.width(text);
+ if (fixed == true) {
+ if (screenPosition >= getWidth()) {
+ position -= Character.charCount(text.codePointBefore(position));
+ screenPosition = StringUtils.width(text) - 1;
+ }
+ } else {
+ windowStart = StringUtils.width(text) - getWidth() + 1;
+ if (windowStart < 0) {
+ windowStart = 0;
+ }
+ }
+ }
+
+ /**
+ * Set the editing position. The field may adjust the window start to
+ * show as much of the field as possible.
+ *
+ * @param position the new position
+ * @throws IndexOutOfBoundsException if position is outside the range of
+ * the available text
+ */
+ public void setPosition(final int position) {
+ if ((position < 0) || (position >= text.length())) {
+ throw new IndexOutOfBoundsException("Max length is " +
+ text.length() + ", requested position " + position);
+ }
+ this.position = position;
+ normalizeWindowStart();
+ }
+
+ /**
+ * Set the active color key.
+ *
+ * @param activeColorKey ColorTheme key color to use when this field is
+ * active
+ */
+ public void setActiveColorKey(final String activeColorKey) {
+ this.activeColorKey = activeColorKey;
+ }
+
+ /**
+ * Set the inactive color key.
+ *
+ * @param inactiveColorKey ColorTheme key color to use when this field is
+ * inactive
+ */
+ public void setInactiveColorKey(final String inactiveColorKey) {
+ this.inactiveColorKey = inactiveColorKey;
+ }
+
+ /**
+ * Set the action to perform when the user presses enter.
+ *
+ * @param action the action to perform when the user presses enter
+ */
+ public void setEnterAction(final TAction action) {
+ enterAction = action;
+ }
+
+ /**
+ * Set the action to perform when the field is updated.
+ *
+ * @param action the action to perform when the field is updated
+ */
+ public void setUpdateAction(final TAction action) {
+ updateAction = action;
+ }
+
+ }
--- /dev/null
+ /*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+ package jexer;
+
+ import java.io.File;
+ import java.io.IOException;
+ import java.util.List;
+ import java.util.ResourceBundle;
+
+ import jexer.backend.SwingTerminal;
+ import jexer.bits.GraphicsChars;
+ import jexer.event.TKeypressEvent;
+ import jexer.ttree.TDirectoryTreeItem;
+ import jexer.ttree.TTreeItem;
+ import jexer.ttree.TTreeViewWidget;
+ import static jexer.TKeypress.*;
+
+ /**
+ * TFileOpenBox is a system-modal dialog for selecting a file to open. Call
+ * it like:
+ *
+ * <pre>
+ * {@code
+ * filename = fileOpenBox("/path/to/file.ext",
+ * TFileOpenBox.Type.OPEN);
+ * if (filename != null) {
+ * ... the user selected a file, go open it ...
+ * }
+ * }
+ * </pre>
+ *
+ */
+ public class TFileOpenBox extends TWindow {
+
+ /**
+ * Translated strings.
+ */
+ private static final ResourceBundle i18n = ResourceBundle.getBundle(TFileOpenBox.class.getName());
+
+ // ------------------------------------------------------------------------
+ // Constants --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * TFileOpenBox can be called for either Open or Save actions.
+ */
+ public enum Type {
+ /**
+ * Button will be labeled "Open".
+ */
+ OPEN,
+
+ /**
+ * Button will be labeled "Save".
+ */
+ SAVE,
+
+ /**
+ * Button will be labeled "Select".
+ */
+ SELECT
+ }
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * String to return, or null if the user canceled.
+ */
+ private String filename = null;
+
+ /**
+ * The left-side tree view pane.
+ */
+ private TTreeViewWidget treeView;
+
+ /**
+ * The data behind treeView.
+ */
+ private TDirectoryTreeItem treeViewRoot;
+
+ /**
+ * The right-side directory list pane.
+ */
+ private TDirectoryList directoryList;
+
+ /**
+ * The top row text field.
+ */
+ private TField entryField;
+
+ /**
+ * The Open or Save button.
+ */
+ private TButton openButton;
+
+ /**
+ * The type of box this is (OPEN, SAVE, or SELECT).
+ */
+ private Type type = Type.OPEN;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor. The file open box will be centered on screen.
+ *
+ * @param application the TApplication that manages this window
+ * @param path path of selected file
+ * @param type one of the Type constants
+ * @throws IOException of a java.io operation throws
+ */
+ public TFileOpenBox(final TApplication application, final String path,
+ final Type type) throws IOException {
+
+ this(application, path, type, null);
+ }
+
+ /**
+ * Public constructor. The file open box will be centered on screen.
+ *
+ * @param application the TApplication that manages this window
+ * @param path path of selected file
+ * @param type one of the Type constants
+ * @param filters a list of strings that files must match to be displayed
+ * @throws IOException of a java.io operation throws
+ */
+ public TFileOpenBox(final TApplication application, final String path,
+ final Type type, final List<String> filters) throws IOException {
+
+ // Register with the TApplication
+ super(application, "", 0, 0, 76, 22, MODAL);
+
+ // Add text field
+ entryField = addField(1, 1, getWidth() - 4, false,
+ (new File(path)).getCanonicalPath(),
+ new TAction() {
+ public void DO() {
+ try {
+ checkFilename(entryField.getText());
+ } catch (IOException e) {
+ // If the backend is Swing, we can emit the stack
+ // trace to stderr. Otherwise, just squash it.
+ if (getScreen() instanceof SwingTerminal) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }, null);
+ entryField.onKeypress(new TKeypressEvent(kbEnd));
+
+ // Add directory treeView
+ treeView = addTreeViewWidget(1, 3, 30, getHeight() - 6,
+ new TAction() {
+ public void DO() {
+ TTreeItem item = treeView.getSelected();
+ File selectedDir = ((TDirectoryTreeItem) item).getFile();
+ try {
+ directoryList.setPath(selectedDir.getCanonicalPath());
+ entryField.setText(selectedDir.getCanonicalPath());
+ if (type == Type.OPEN) {
+ openButton.setEnabled(false);
+ }
+ activate(treeView);
+ } catch (IOException e) {
+ // If the backend is Swing, we can emit the stack
+ // trace to stderr. Otherwise, just squash it.
+ if (getScreen() instanceof SwingTerminal) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+ );
+ treeViewRoot = new TDirectoryTreeItem(treeView, path, true);
+
+ // Add directory files list
+ directoryList = addDirectoryList(path, 34, 3, 28, getHeight() - 6,
+ new TAction() {
+ public void DO() {
+ try {
+ File newPath = directoryList.getPath();
+ entryField.setText(newPath.getCanonicalPath());
+ entryField.onKeypress(new TKeypressEvent(kbEnd));
+ openButton.setEnabled(true);
+ activate(entryField);
+ checkFilename(entryField.getText());
+ } catch (IOException e) {
+ // If the backend is Swing, we can emit the stack
+ // trace to stderr. Otherwise, just squash it.
+ if (getScreen() instanceof SwingTerminal) {
+ e.printStackTrace();
+ }
+ }
+ }
+ },
+ new TAction() {
+ public void DO() {
+ try {
+ File newPath = directoryList.getPath();
+ entryField.setText(newPath.getCanonicalPath());
+ entryField.onKeypress(new TKeypressEvent(kbEnd));
+ openButton.setEnabled(true);
+ activate(entryField);
+ } catch (IOException e) {
+ // If the backend is Swing, we can emit the stack
+ // trace to stderr. Otherwise, just squash it.
+ if (getScreen() instanceof SwingTerminal) {
+ e.printStackTrace();
+ }
+ }
+ }
+ },
+ filters);
+
+ String openLabel = "";
+ switch (type) {
+ case OPEN:
+ openLabel = i18n.getString("openButton");
+ setTitle(i18n.getString("openTitle"));
+ break;
+ case SAVE:
+ openLabel = i18n.getString("saveButton");
+ setTitle(i18n.getString("saveTitle"));
+ break;
+ case SELECT:
+ openLabel = i18n.getString("selectButton");
+ setTitle(i18n.getString("selectTitle"));
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid type: " + type);
+ }
+ this.type = type;
+
+ // Setup button actions
+ openButton = addButton(openLabel, this.getWidth() - 12, 3,
+ new TAction() {
+ public void DO() {
+ try {
+ checkFilename(entryField.getText());
+ } catch (IOException e) {
+ // If the backend is Swing, we can emit the stack
+ // trace to stderr. Otherwise, just squash it.
+ if (getScreen() instanceof SwingTerminal) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+ );
+ if (type == Type.OPEN) {
+ openButton.setEnabled(false);
+ }
+
+ addButton(i18n.getString("cancelButton"), getWidth() - 12, 5,
+ new TAction() {
+ public void DO() {
+ filename = null;
+ getApplication().closeWindow(TFileOpenBox.this);
+ }
+ }
+ );
+
+ // Default to the directory list
+ activate(directoryList);
+
+ // Set the secondaryFiber to run me
+ getApplication().enableSecondaryEventReceiver(this);
+
+ // Yield to the secondary thread. When I come back from the
+ // constructor response will already be set.
+ getApplication().yield();
+ }
+
+ // ------------------------------------------------------------------------
+ // Event handlers ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Handle keystrokes.
+ *
+ * @param keypress keystroke event
+ */
+ @Override
+ public void onKeypress(final TKeypressEvent keypress) {
+ // Escape - behave like cancel
+ if (keypress.equals(kbEsc)) {
+ // Close window
+ filename = null;
+ getApplication().closeWindow(this);
+ return;
+ }
+
+ if (treeView.isActive()) {
+ if ((keypress.equals(kbEnter))
+ || (keypress.equals(kbUp))
+ || (keypress.equals(kbDown))
+ || (keypress.equals(kbPgUp))
+ || (keypress.equals(kbPgDn))
+ || (keypress.equals(kbHome))
+ || (keypress.equals(kbEnd))
+ ) {
+ // Tree view will be changing, update the directory list.
+ super.onKeypress(keypress);
+
+ // This is the same action as treeView's enter.
+ TTreeItem item = treeView.getSelected();
+ File selectedDir = ((TDirectoryTreeItem) item).getFile();
+ try {
+ directoryList.setPath(selectedDir.getCanonicalPath());
+ if (type == Type.OPEN) {
+ openButton.setEnabled(false);
+ }
+ activate(treeView);
+ } catch (IOException e) {
+ // If the backend is Swing, we can emit the stack trace
+ // to stderr. Otherwise, just squash it.
+ if (getScreen() instanceof SwingTerminal) {
+ e.printStackTrace();
+ }
+ }
+ return;
+ }
+ }
+
+ // Pass to my parent
+ super.onKeypress(keypress);
+ }
+
+ // ------------------------------------------------------------------------
+ // TWidget ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Draw me on screen.
+ */
+ @Override
+ public void draw() {
+ super.draw();
+ vLineXY(33, 4, getHeight() - 6, GraphicsChars.WINDOW_SIDE,
+ getBackground());
+ }
+
+ // ------------------------------------------------------------------------
+ // TFileOpenBox -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get the return string.
+ *
+ * @return the filename the user selected, or null if they canceled.
+ */
+ public String getFilename() {
+ return filename;
+ }
+
+ /**
+ * See if there is a valid filename to return. If the filename is a
+ * directory, then
+ *
+ * @param newFilename the filename to check and return
+ * @throws IOException of a java.io operation throws
+ */
+ private void checkFilename(final String newFilename) throws IOException {
+ File newFile = new File(newFilename);
+ if (newFile.exists()) {
+ if (newFile.isFile() || (type == Type.SELECT)) {
+ filename = newFilename;
+ getApplication().closeWindow(this);
+ return;
+ }
+ if (newFile.isDirectory()) {
+ treeViewRoot = new TDirectoryTreeItem(treeView,
+ newFilename, true);
+ treeView.setTreeRoot(treeViewRoot, true);
+ if (type == Type.OPEN) {
+ openButton.setEnabled(false);
+ }
+ directoryList.setPath(newFilename);
+ }
+ } else if (type != Type.OPEN) {
+ filename = newFilename;
+ getApplication().closeWindow(this);
+ return;
+ }
+ }
+
+ }
--- /dev/null
+ openButton=\ &Open\
+ openTitle=Open File...
+ saveButton=\ &Save\
+ saveTitle=Save File...
+ cancelButton=&Cancel
+ selectButton=S&elect
+ selectTitle=Select File...
--- /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.awt.Font;
+ import java.awt.GraphicsEnvironment;
+ import java.util.ArrayList;
+ import java.util.Arrays;
+ import java.util.List;
+ import java.util.ResourceBundle;
+
+ import jexer.backend.ECMA48Terminal;
+ import jexer.backend.SwingTerminal;
+ import jexer.bits.CellAttributes;
+ import jexer.bits.GraphicsChars;
+ import jexer.event.TKeypressEvent;
+ import static jexer.TKeypress.*;
+
+ /**
+ * TFontChooserWindow provides an easy UI for users to alter the running
+ * font.
+ *
+ */
+ public class TFontChooserWindow extends TWindow {
+
+ /**
+ * Translated strings.
+ */
+ private static final ResourceBundle i18n = ResourceBundle.getBundle(TFontChooserWindow.class.getName());
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The Swing screen.
+ */
+ private SwingTerminal terminal = null;
+
+ /**
+ * The ECMA48 screen.
+ */
+ private ECMA48Terminal ecmaTerminal = null;
+
+ /**
+ * The font name.
+ */
+ private TComboBox fontName;
+
+ /**
+ * The font size.
+ */
+ private TField fontSize;
+
+ /**
+ * The X text adjustment.
+ */
+ private TField textAdjustX;
+
+ /**
+ * The Y text adjustment.
+ */
+ private TField textAdjustY;
+
+ /**
+ * The height text adjustment.
+ */
+ private TField textAdjustHeight;
+
+ /**
+ * The width text adjustment.
+ */
+ private TField textAdjustWidth;
+
+ /**
+ * The sixel palette size.
+ */
+ private TComboBox sixelPaletteSize;
+
+ /**
+ * The original font size.
+ */
+ private int oldFontSize = 20;
+
+ /**
+ * The original font.
+ */
+ private Font oldFont = null;
+
+ /**
+ * The original text adjust X value.
+ */
+ private int oldTextAdjustX = 0;
+
+ /**
+ * The original text adjust Y value.
+ */
+ private int oldTextAdjustY = 0;
+
+ /**
+ * The original text adjust height value.
+ */
+ private int oldTextAdjustHeight = 0;
+
+ /**
+ * The original text adjust width value.
+ */
+ private int oldTextAdjustWidth = 0;
+
+ /**
+ * The original sixel palette (number of colors) value.
+ */
+ private int oldSixelPaletteSize = 1024;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor. The window will be centered on screen.
+ *
+ * @param application the TApplication that manages this window
+ */
+ public TFontChooserWindow(final TApplication application) {
+
+ // Register with the TApplication
+ super(application, i18n.getString("windowTitle"), 0, 0, 60, 21, MODAL);
+
+ // Add shortcut text
+ newStatusBar(i18n.getString("statusBar"));
+
+ if (getScreen() instanceof SwingTerminal) {
+ terminal = (SwingTerminal) getScreen();
+ }
+ if (getScreen() instanceof ECMA48Terminal) {
+ ecmaTerminal = (ECMA48Terminal) getScreen();
+ }
+
+ addLabel(i18n.getString("fontName"), 1, 1, "ttext", false);
+ addLabel(i18n.getString("fontSize"), 1, 2, "ttext", false);
+ addLabel(i18n.getString("textAdjustX"), 1, 4, "ttext", false);
+ addLabel(i18n.getString("textAdjustY"), 1, 5, "ttext", false);
+ addLabel(i18n.getString("textAdjustHeight"), 1, 6, "ttext", false);
+ addLabel(i18n.getString("textAdjustWidth"), 1, 7, "ttext", false);
+ addLabel(i18n.getString("sixelPaletteSize"), 1, 9, "ttext", false);
+
+ int col = 21;
+ if (terminal == null) {
+ // Non-Swing case: we can't change anything
+ addLabel(i18n.getString("unavailable"), col, 1);
+ addLabel(i18n.getString("unavailable"), col, 2);
+ addLabel(i18n.getString("unavailable"), col, 4);
+ addLabel(i18n.getString("unavailable"), col, 5);
+ addLabel(i18n.getString("unavailable"), col, 6);
+ addLabel(i18n.getString("unavailable"), col, 7);
+ }
+ if (ecmaTerminal == null) {
+ addLabel(i18n.getString("unavailable"), col, 9);
+ }
+ if (ecmaTerminal != null) {
+ oldSixelPaletteSize = ecmaTerminal.getSixelPaletteSize();
+
+ String [] sixelSizes = { "2", "256", "512", "1024", "2048" };
+ List<String> sizes = new ArrayList<String>();
+ sizes.addAll(Arrays.asList(sixelSizes));
+ sixelPaletteSize = addComboBox(col, 9, 10, sizes, 0, 6,
+ new TAction() {
+ public void DO() {
+ try {
+ ecmaTerminal.setSixelPaletteSize(Integer.parseInt(
+ sixelPaletteSize.getText()));
+ } catch (NumberFormatException e) {
+ // SQUASH
+ }
+ }
+ }
+ );
+ sixelPaletteSize.setText(Integer.toString(oldSixelPaletteSize));
+ }
+
+ if (terminal != null) {
+ oldFont = terminal.getFont();
+ oldFontSize = terminal.getFontSize();
+ oldTextAdjustX = terminal.getTextAdjustX();
+ oldTextAdjustY = terminal.getTextAdjustY();
+ oldTextAdjustHeight = terminal.getTextAdjustHeight();
+ oldTextAdjustWidth = terminal.getTextAdjustWidth();
+
+ String [] fontNames = GraphicsEnvironment.
+ getLocalGraphicsEnvironment().getAvailableFontFamilyNames();
+ List<String> fonts = new ArrayList<String>();
+ fonts.add(0, i18n.getString("builtInTerminus"));
+ fonts.addAll(Arrays.asList(fontNames));
+ fontName = addComboBox(col, 1, 25, fonts, 0, 10,
+ new TAction() {
+ public void DO() {
+ if (fontName.getText().equals(i18n.
+ getString("builtInTerminus"))) {
+
+ terminal.setDefaultFont();
+ } else {
+ terminal.setFont(new Font(fontName.getText(),
+ Font.PLAIN, terminal.getFontSize()));
+ fontSize.setText(Integer.toString(
+ terminal.getFontSize()));
+ textAdjustX.setText(Integer.toString(
+ terminal.getTextAdjustX()));
+ textAdjustY.setText(Integer.toString(
+ terminal.getTextAdjustY()));
+ textAdjustHeight.setText(Integer.toString(
+ terminal.getTextAdjustHeight()));
+ textAdjustWidth.setText(Integer.toString(
+ terminal.getTextAdjustWidth()));
+ }
+ }
+ }
+ );
+
+ // Font size
+ fontSize = addField(col, 2, 3, true,
+ Integer.toString(terminal.getFontSize()),
+ new TAction() {
+ public void DO() {
+ int currentSize = terminal.getFontSize();
+ int newSize = currentSize;
+ try {
+ newSize = Integer.parseInt(fontSize.getText());
+ } catch (NumberFormatException e) {
+ fontSize.setText(Integer.toString(currentSize));
+ }
+ if (newSize != currentSize) {
+ terminal.setFontSize(newSize);
+ textAdjustX.setText(Integer.toString(
+ terminal.getTextAdjustX()));
+ textAdjustY.setText(Integer.toString(
+ terminal.getTextAdjustY()));
+ textAdjustHeight.setText(Integer.toString(
+ terminal.getTextAdjustHeight()));
+ textAdjustWidth.setText(Integer.toString(
+ terminal.getTextAdjustWidth()));
+ }
+ }
+ },
+ null);
+
+ addSpinner(col + 3, 2,
+ new TAction() {
+ public void DO() {
+ int currentSize = terminal.getFontSize();
+ int newSize = currentSize;
+ try {
+ newSize = Integer.parseInt(fontSize.getText());
+ newSize++;
+ } catch (NumberFormatException e) {
+ fontSize.setText(Integer.toString(currentSize));
+ }
+ fontSize.setText(Integer.toString(newSize));
+ if (newSize != currentSize) {
+ terminal.setFontSize(newSize);
+ textAdjustX.setText(Integer.toString(
+ terminal.getTextAdjustX()));
+ textAdjustY.setText(Integer.toString(
+ terminal.getTextAdjustY()));
+ textAdjustHeight.setText(Integer.toString(
+ terminal.getTextAdjustHeight()));
+ textAdjustWidth.setText(Integer.toString(
+ terminal.getTextAdjustWidth()));
+ }
+ }
+ },
+ new TAction() {
+ public void DO() {
+ int currentSize = terminal.getFontSize();
+ int newSize = currentSize;
+ try {
+ newSize = Integer.parseInt(fontSize.getText());
+ newSize--;
+ } catch (NumberFormatException e) {
+ fontSize.setText(Integer.toString(currentSize));
+ }
+ fontSize.setText(Integer.toString(newSize));
+ if (newSize != currentSize) {
+ terminal.setFontSize(newSize);
+ textAdjustX.setText(Integer.toString(
+ terminal.getTextAdjustX()));
+ textAdjustY.setText(Integer.toString(
+ terminal.getTextAdjustY()));
+ textAdjustHeight.setText(Integer.toString(
+ terminal.getTextAdjustHeight()));
+ textAdjustWidth.setText(Integer.toString(
+ terminal.getTextAdjustWidth()));
+ }
+ }
+ }
+ );
+
+ // textAdjustX
+ textAdjustX = addField(col, 4, 3, true,
+ Integer.toString(terminal.getTextAdjustX()),
+ new TAction() {
+ public void DO() {
+ int currentAdjust = terminal.getTextAdjustX();
+ int newAdjust = currentAdjust;
+ try {
+ newAdjust = Integer.parseInt(textAdjustX.getText());
+ } catch (NumberFormatException e) {
+ textAdjustX.setText(Integer.toString(currentAdjust));
+ }
+ if (newAdjust != currentAdjust) {
+ terminal.setTextAdjustX(newAdjust);
+ }
+ }
+ },
+ null);
+
+ addSpinner(col + 3, 4,
+ new TAction() {
+ public void DO() {
+ int currentAdjust = terminal.getTextAdjustX();
+ int newAdjust = currentAdjust;
+ try {
+ newAdjust = Integer.parseInt(textAdjustX.getText());
+ newAdjust++;
+ } catch (NumberFormatException e) {
+ textAdjustX.setText(Integer.toString(currentAdjust));
+ }
+ textAdjustX.setText(Integer.toString(newAdjust));
+ if (newAdjust != currentAdjust) {
+ terminal.setTextAdjustX(newAdjust);
+ }
+ }
+ },
+ new TAction() {
+ public void DO() {
+ int currentAdjust = terminal.getTextAdjustX();
+ int newAdjust = currentAdjust;
+ try {
+ newAdjust = Integer.parseInt(textAdjustX.getText());
+ newAdjust--;
+ } catch (NumberFormatException e) {
+ textAdjustX.setText(Integer.toString(currentAdjust));
+ }
+ textAdjustX.setText(Integer.toString(newAdjust));
+ if (newAdjust != currentAdjust) {
+ terminal.setTextAdjustX(newAdjust);
+ }
+ }
+ }
+ );
+
+ // textAdjustY
+ textAdjustY = addField(col, 5, 3, true,
+ Integer.toString(terminal.getTextAdjustY()),
+ new TAction() {
+ public void DO() {
+ int currentAdjust = terminal.getTextAdjustY();
+ int newAdjust = currentAdjust;
+ try {
+ newAdjust = Integer.parseInt(textAdjustY.getText());
+ } catch (NumberFormatException e) {
+ textAdjustY.setText(Integer.toString(currentAdjust));
+ }
+ if (newAdjust != currentAdjust) {
+ terminal.setTextAdjustY(newAdjust);
+ }
+ }
+ },
+ null);
+
+ addSpinner(col + 3, 5,
+ new TAction() {
+ public void DO() {
+ int currentAdjust = terminal.getTextAdjustY();
+ int newAdjust = currentAdjust;
+ try {
+ newAdjust = Integer.parseInt(textAdjustY.getText());
+ newAdjust++;
+ } catch (NumberFormatException e) {
+ textAdjustY.setText(Integer.toString(currentAdjust));
+ }
+ textAdjustY.setText(Integer.toString(newAdjust));
+ if (newAdjust != currentAdjust) {
+ terminal.setTextAdjustY(newAdjust);
+ }
+ }
+ },
+ new TAction() {
+ public void DO() {
+ int currentAdjust = terminal.getTextAdjustY();
+ int newAdjust = currentAdjust;
+ try {
+ newAdjust = Integer.parseInt(textAdjustY.getText());
+ newAdjust--;
+ } catch (NumberFormatException e) {
+ textAdjustY.setText(Integer.toString(currentAdjust));
+ }
+ textAdjustY.setText(Integer.toString(newAdjust));
+ if (newAdjust != currentAdjust) {
+ terminal.setTextAdjustY(newAdjust);
+ }
+ }
+ }
+ );
+
+ // textAdjustHeight
+ textAdjustHeight = addField(col, 6, 3, true,
+ Integer.toString(terminal.getTextAdjustHeight()),
+ new TAction() {
+ public void DO() {
+ int currentAdjust = terminal.getTextAdjustHeight();
+ int newAdjust = currentAdjust;
+ try {
+ newAdjust = Integer.parseInt(textAdjustHeight.getText());
+ } catch (NumberFormatException e) {
+ textAdjustHeight.setText(Integer.toString(currentAdjust));
+ }
+ if (newAdjust != currentAdjust) {
+ terminal.setTextAdjustHeight(newAdjust);
+ }
+ }
+ },
+ null);
+
+ addSpinner(col + 3, 6,
+ new TAction() {
+ public void DO() {
+ int currentAdjust = terminal.getTextAdjustHeight();
+ int newAdjust = currentAdjust;
+ try {
+ newAdjust = Integer.parseInt(textAdjustHeight.getText());
+ newAdjust++;
+ } catch (NumberFormatException e) {
+ textAdjustHeight.setText(Integer.toString(currentAdjust));
+ }
+ textAdjustHeight.setText(Integer.toString(newAdjust));
+ if (newAdjust != currentAdjust) {
+ terminal.setTextAdjustHeight(newAdjust);
+ }
+ }
+ },
+ new TAction() {
+ public void DO() {
+ int currentAdjust = terminal.getTextAdjustHeight();
+ int newAdjust = currentAdjust;
+ try {
+ newAdjust = Integer.parseInt(textAdjustHeight.getText());
+ newAdjust--;
+ } catch (NumberFormatException e) {
+ textAdjustHeight.setText(Integer.toString(currentAdjust));
+ }
+ textAdjustHeight.setText(Integer.toString(newAdjust));
+ if (newAdjust != currentAdjust) {
+ terminal.setTextAdjustHeight(newAdjust);
+ }
+ }
+ }
+ );
+
+ // textAdjustWidth
+ textAdjustWidth = addField(col, 7, 3, true,
+ Integer.toString(terminal.getTextAdjustWidth()),
+ new TAction() {
+ public void DO() {
+ int currentAdjust = terminal.getTextAdjustWidth();
+ int newAdjust = currentAdjust;
+ try {
+ newAdjust = Integer.parseInt(textAdjustWidth.getText());
+ } catch (NumberFormatException e) {
+ textAdjustWidth.setText(Integer.toString(currentAdjust));
+ }
+ if (newAdjust != currentAdjust) {
+ terminal.setTextAdjustWidth(newAdjust);
+ }
+ }
+ },
+ null);
+
+ addSpinner(col + 3, 7,
+ new TAction() {
+ public void DO() {
+ int currentAdjust = terminal.getTextAdjustWidth();
+ int newAdjust = currentAdjust;
+ try {
+ newAdjust = Integer.parseInt(textAdjustWidth.getText());
+ newAdjust++;
+ } catch (NumberFormatException e) {
+ textAdjustWidth.setText(Integer.toString(currentAdjust));
+ }
+ textAdjustWidth.setText(Integer.toString(newAdjust));
+ if (newAdjust != currentAdjust) {
+ terminal.setTextAdjustWidth(newAdjust);
+ }
+ }
+ },
+ new TAction() {
+ public void DO() {
+ int currentAdjust = terminal.getTextAdjustWidth();
+ int newAdjust = currentAdjust;
+ try {
+ newAdjust = Integer.parseInt(textAdjustWidth.getText());
+ newAdjust--;
+ } catch (NumberFormatException e) {
+ textAdjustWidth.setText(Integer.toString(currentAdjust));
+ }
+ textAdjustWidth.setText(Integer.toString(newAdjust));
+ if (newAdjust != currentAdjust) {
+ terminal.setTextAdjustWidth(newAdjust);
+ }
+ }
+ }
+ );
+
+ }
+
+ addButton(i18n.getString("okButton"), 18, getHeight() - 4,
+ new TAction() {
+ public void DO() {
+ // Close window.
+ TFontChooserWindow.this.close();
+ }
+ });
+
+ TButton cancelButton = addButton(i18n.getString("cancelButton"),
+ 30, getHeight() - 4,
+ new TAction() {
+ public void DO() {
+ // Restore old values, then close the window.
+ if (terminal != null) {
+ terminal.setFont(oldFont);
+ terminal.setFontSize(oldFontSize);
+ terminal.setTextAdjustX(oldTextAdjustX);
+ terminal.setTextAdjustY(oldTextAdjustY);
+ terminal.setTextAdjustHeight(oldTextAdjustHeight);
+ terminal.setTextAdjustWidth(oldTextAdjustWidth);
+ }
+ if (ecmaTerminal != null) {
+ ecmaTerminal.setSixelPaletteSize(oldSixelPaletteSize);
+ }
+ TFontChooserWindow.this.close();
+ }
+ });
+
+ // Save this for last: make the cancel button default action.
+ activate(cancelButton);
+
+ }
+
+ // ------------------------------------------------------------------------
+ // Event handlers ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Handle keystrokes.
+ *
+ * @param keypress keystroke event
+ */
+ @Override
+ public void onKeypress(final TKeypressEvent keypress) {
+ // Escape - behave like cancel
+ if (keypress.equals(kbEsc)) {
+ // Restore old values, then close the window.
+ if (terminal != null) {
+ terminal.setFont(oldFont);
+ terminal.setFontSize(oldFontSize);
+ }
+ if (ecmaTerminal != null) {
+ ecmaTerminal.setSixelPaletteSize(oldSixelPaletteSize);
+ }
+ getApplication().closeWindow(this);
+ return;
+ }
+
+ // Pass to my parent
+ super.onKeypress(keypress);
+ }
+
+ // ------------------------------------------------------------------------
+ // TWindow ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Draw me on screen.
+ */
+ @Override
+ public void draw() {
+ super.draw();
+
+ int left = 34;
+ CellAttributes color = getTheme().getColor("ttext");
+ drawBox(left, 6, left + 24, 14, color, color, 3, false);
+ putStringXY(left + 2, 6, i18n.getString("sample"), color);
+ for (int i = 7; i < 13; i++) {
+ hLineXY(left + 1, i, 22, GraphicsChars.HATCH, color);
+ }
+
+ }
+
+ // ------------------------------------------------------------------------
+ // TFontChooserWindow -----------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ }
--- /dev/null
+ windowTitle=Screen
+ okButton=\ \ &OK\ \
+ cancelButton=&Cancel
+ statusBar=Select Screen Options
+
+ fontName=Font name:
+ fontSize=Font size:
+ textAdjustX=X adjust:
+ textAdjustY=Y adjust:
+ textAdjustHeight=Height adjust:
+ textAdjustWidth=Width adjust:
+
+ sixelPaletteSize=Sixel Palette Size:
+
+ unavailable=Unavailable
+ builtInTerminus=Built-In Terminus
+ sample=\ Sample Window\
--- /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 jexer.bits.CellAttributes;
+ import jexer.bits.GraphicsChars;
+ import jexer.event.TMouseEvent;
+
+ /**
+ * THScroller implements a simple horizontal scroll bar.
+ */
+ public class THScroller extends TWidget {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Value that corresponds to being on the left edge of the scroll bar.
+ */
+ private int leftValue = 0;
+
+ /**
+ * Value that corresponds to being on the right edge of the scroll bar.
+ */
+ private int rightValue = 100;
+
+ /**
+ * Current value of the scroll.
+ */
+ private int value = 0;
+
+ /**
+ * The increment for clicking on an arrow.
+ */
+ private int smallChange = 1;
+
+ /**
+ * The increment for clicking in the bar between the box and an arrow.
+ */
+ private int bigChange = 20;
+
+ /**
+ * When true, the user is dragging the scroll box.
+ */
+ private boolean inScroll = false;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width height of scroll bar
+ */
+ public THScroller(final TWidget parent, final int x, final int y,
+ final int width) {
+
+ // Set parent and window
+ super(parent, x, y, width, 1);
+ }
+
+ // ------------------------------------------------------------------------
+ // Event handlers ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Handle mouse button releases.
+ *
+ * @param mouse mouse button release event
+ */
+ @Override
+ public void onMouseUp(final TMouseEvent mouse) {
+
+ if (inScroll) {
+ inScroll = false;
+ return;
+ }
+
+ if (rightValue == leftValue) {
+ return;
+ }
+
+ if ((mouse.getX() == 0)
+ && (mouse.getY() == 0)
+ ) {
+ // Clicked on the left arrow
+ decrement();
+ return;
+ }
+
+ if ((mouse.getY() == 0)
+ && (mouse.getX() == getWidth() - 1)
+ ) {
+ // Clicked on the right arrow
+ increment();
+ return;
+ }
+
+ if ((mouse.getY() == 0)
+ && (mouse.getX() > 0)
+ && (mouse.getX() < boxPosition())
+ ) {
+ // Clicked between the left arrow and the box
+ value -= bigChange;
+ if (value < leftValue) {
+ value = leftValue;
+ }
+ return;
+ }
+
+ if ((mouse.getY() == 0)
+ && (mouse.getX() > boxPosition())
+ && (mouse.getX() < getWidth() - 1)
+ ) {
+ // Clicked between the box and the right arrow
+ value += bigChange;
+ if (value > rightValue) {
+ value = rightValue;
+ }
+ return;
+ }
+ }
+
+ /**
+ * Handle mouse movement events.
+ *
+ * @param mouse mouse motion event
+ */
+ @Override
+ public void onMouseMotion(final TMouseEvent mouse) {
+
+ if (rightValue == leftValue) {
+ inScroll = false;
+ return;
+ }
+
+ if ((mouse.isMouse1())
+ && (inScroll)
+ && (mouse.getX() > 0)
+ && (mouse.getX() < getWidth() - 1)
+ ) {
+ // Recompute value based on new box position
+ value = (rightValue - leftValue)
+ * (mouse.getX()) / (getWidth() - 3) + leftValue;
+ if (value > rightValue) {
+ value = rightValue;
+ }
+ if (value < leftValue) {
+ value = leftValue;
+ }
+ return;
+ }
+ inScroll = false;
+ }
+
+ /**
+ * Handle mouse button press events.
+ *
+ * @param mouse mouse button press event
+ */
+ @Override
+ public void onMouseDown(final TMouseEvent mouse) {
+ if (rightValue == leftValue) {
+ inScroll = false;
+ return;
+ }
+
+ if ((mouse.getY() == 0)
+ && (mouse.getX() == boxPosition())
+ ) {
+ inScroll = true;
+ return;
+ }
+
+ }
+
+ // ------------------------------------------------------------------------
+ // TWidget ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Draw a horizontal scroll bar.
+ */
+ @Override
+ public void draw() {
+ CellAttributes arrowColor = getTheme().getColor("tscroller.arrows");
+ CellAttributes barColor = getTheme().getColor("tscroller.bar");
+ putCharXY(0, 0, GraphicsChars.CP437[0x11], arrowColor);
+ putCharXY(getWidth() - 1, 0, GraphicsChars.CP437[0x10], arrowColor);
+
+ // Place the box
+ if (rightValue > leftValue) {
+ hLineXY(1, 0, getWidth() - 2, GraphicsChars.CP437[0xB1], barColor);
+ putCharXY(boxPosition(), 0, GraphicsChars.BOX, arrowColor);
+ } else {
+ hLineXY(1, 0, getWidth() - 2, GraphicsChars.HATCH, barColor);
+ }
+
+ }
+
+ // ------------------------------------------------------------------------
+ // THScroller -------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get the value that corresponds to being on the left edge of the scroll
+ * bar.
+ *
+ * @return the scroll value
+ */
+ public int getLeftValue() {
+ return leftValue;
+ }
+
+ /**
+ * Set the value that corresponds to being on the left edge of the
+ * scroll bar.
+ *
+ * @param leftValue the new scroll value
+ */
+ public void setLeftValue(final int leftValue) {
+ this.leftValue = leftValue;
+ }
+
+ /**
+ * Get the value that corresponds to being on the right edge of the
+ * scroll bar.
+ *
+ * @return the scroll value
+ */
+ public int getRightValue() {
+ return rightValue;
+ }
+
+ /**
+ * Set the value that corresponds to being on the right edge of the
+ * scroll bar.
+ *
+ * @param rightValue the new scroll value
+ */
+ public void setRightValue(final int rightValue) {
+ this.rightValue = rightValue;
+ }
+
+ /**
+ * Get current value of the scroll.
+ *
+ * @return the scroll value
+ */
+ public int getValue() {
+ return value;
+ }
+
+ /**
+ * Set current value of the scroll.
+ *
+ * @param value the new scroll value
+ */
+ public void setValue(final int value) {
+ this.value = value;
+ }
+
+ /**
+ * Get the increment for clicking on an arrow.
+ *
+ * @return the increment value
+ */
+ public int getSmallChange() {
+ return smallChange;
+ }
+
+ /**
+ * Set the increment for clicking on an arrow.
+ *
+ * @param smallChange the new increment value
+ */
+ public void setSmallChange(final int smallChange) {
+ this.smallChange = smallChange;
+ }
+
+ /**
+ * Set the increment for clicking in the bar between the box and an
+ * arrow.
+ *
+ * @return the increment value
+ */
+ public int getBigChange() {
+ return bigChange;
+ }
+
+ /**
+ * Set the increment for clicking in the bar between the box and an
+ * arrow.
+ *
+ * @param bigChange the new increment value
+ */
+ public void setBigChange(final int bigChange) {
+ this.bigChange = bigChange;
+ }
+
+ /**
+ * Compute the position of the scroll box (a.k.a. grip, thumb).
+ *
+ * @return Y position of the box, between 1 and width - 2
+ */
+ private int boxPosition() {
+ return (getWidth() - 3) * (value - leftValue) / (rightValue - leftValue) + 1;
+ }
+
+ /**
+ * Perform a small step change left.
+ */
+ public void decrement() {
+ if (leftValue == rightValue) {
+ return;
+ }
+ value -= smallChange;
+ if (value < leftValue) {
+ value = leftValue;
+ }
+ }
+
+ /**
+ * Perform a small step change right.
+ */
+ public void increment() {
+ if (leftValue == rightValue) {
+ return;
+ }
+ value += smallChange;
+ if (value > rightValue) {
+ value = rightValue;
+ }
+ }
+
+ /**
+ * Perform a big step change left.
+ */
+ public void bigDecrement() {
+ if (leftValue == rightValue) {
+ return;
+ }
+ value -= bigChange;
+ if (value < leftValue) {
+ value = leftValue;
+ }
+ }
+
+ /**
+ * Perform a big step change right.
+ */
+ public void bigIncrement() {
+ if (rightValue == leftValue) {
+ return;
+ }
+ value += bigChange;
+ if (value > rightValue) {
+ value = rightValue;
+ }
+ }
+
+ /**
+ * Go to the left edge of the scroller.
+ */
+ public void toLeft() {
+ value = leftValue;
+ }
+
+ /**
+ * Go to the right edge of the scroller.
+ */
+ public void toRight() {
+ value = rightValue;
+ }
+
+ }
--- /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.awt.image.BufferedImage;
+
+ import jexer.backend.ECMA48Terminal;
+ import jexer.backend.MultiScreen;
+ import jexer.backend.SwingTerminal;
+ import jexer.bits.Cell;
+ import jexer.event.TKeypressEvent;
+ import jexer.event.TMouseEvent;
+ import jexer.event.TResizeEvent;
+ import static jexer.TKeypress.*;
+
+ /**
+ * TImage renders a piece of a bitmap image on screen.
+ */
+ public class TImage extends TWidget {
+
+ // ------------------------------------------------------------------------
+ // Constants --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Selections for fitting the image to the text cells.
+ */
+ public enum Scale {
+ /**
+ * No scaling.
+ */
+ NONE,
+
+ /**
+ * Stretch/shrink the image in both directions to fully fill the text
+ * area width/height.
+ */
+ STRETCH,
+
+ /**
+ * Scale the image, preserving aspect ratio, to fill the text area
+ * width/height (like letterbox). The background color for the
+ * letterboxed area is specified in scaleBackColor.
+ */
+ SCALE,
+ }
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Scaling strategy to use.
+ */
+ private Scale scale = Scale.NONE;
+
+ /**
+ * Scaling strategy to use.
+ */
+ private java.awt.Color scaleBackColor = java.awt.Color.BLACK;
+
+ /**
+ * The action to perform when the user clicks on the image.
+ */
+ private TAction clickAction;
+
+ /**
+ * The image to display.
+ */
+ private BufferedImage image;
+
+ /**
+ * The original image from construction time.
+ */
+ private BufferedImage originalImage;
+
+ /**
+ * The current scaling factor for the image.
+ */
+ private double scaleFactor = 1.0;
+
+ /**
+ * The current clockwise rotation for the image.
+ */
+ private int clockwise = 0;
+
+ /**
+ * If true, this widget was resized and a new scaled image must be
+ * produced.
+ */
+ private boolean resized = false;
+
+ /**
+ * Left column of the image. 0 is the left-most column.
+ */
+ private int left;
+
+ /**
+ * Top row of the image. 0 is the top-most row.
+ */
+ private int top;
+
+ /**
+ * The cells containing the broken up image pieces.
+ */
+ private Cell cells[][];
+
+ /**
+ * The number of rows in cells[].
+ */
+ private int cellRows;
+
+ /**
+ * The number of columns in cells[].
+ */
+ private int cellColumns;
+
+ /**
+ * Last text width value.
+ */
+ private int lastTextWidth = -1;
+
+ /**
+ * Last text height value.
+ */
+ private int lastTextHeight = -1;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width number of text cells for width of the image
+ * @param height number of text cells for height of the image
+ * @param image the image to display
+ * @param left left column of the image. 0 is the left-most column.
+ * @param top top row of the image. 0 is the top-most row.
+ */
+ public TImage(final TWidget parent, final int x, final int y,
+ final int width, final int height,
+ final BufferedImage image, final int left, final int top) {
+
+ this(parent, x, y, width, height, image, left, top, null);
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width number of text cells for width of the image
+ * @param height number of text cells for height of the image
+ * @param image the image to display
+ * @param left left column of the image. 0 is the left-most column.
+ * @param top top row of the image. 0 is the top-most row.
+ * @param clickAction function to call when mouse is pressed
+ */
+ public TImage(final TWidget parent, final int x, final int y,
+ final int width, final int height,
+ final BufferedImage image, final int left, final int top,
+ final TAction clickAction) {
+
+ // Set parent and window
+ super(parent, x, y, width, height);
+
+ setCursorVisible(false);
+ this.originalImage = image;
+ this.left = left;
+ this.top = top;
+ this.clickAction = clickAction;
+
+ sizeToImage(true);
+ }
+
+ // ------------------------------------------------------------------------
+ // Event handlers ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Handle mouse press events.
+ *
+ * @param mouse mouse button press event
+ */
+ @Override
+ public void onMouseDown(final TMouseEvent mouse) {
+ if (clickAction != null) {
+ clickAction.DO(this);
+ return;
+ }
+ }
+
+ /**
+ * Handle keystrokes.
+ *
+ * @param keypress keystroke event
+ */
+ @Override
+ public void onKeypress(final TKeypressEvent keypress) {
+ if (!keypress.getKey().isFnKey()) {
+ if (keypress.getKey().getChar() == '+') {
+ // Make the image bigger.
+ scaleFactor *= 1.25;
+ image = null;
+ sizeToImage(true);
+ return;
+ }
+ if (keypress.getKey().getChar() == '-') {
+ // Make the image smaller.
+ scaleFactor *= 0.80;
+ image = null;
+ sizeToImage(true);
+ return;
+ }
+ }
+ if (keypress.equals(kbAltUp)) {
+ // Make the image bigger.
+ scaleFactor *= 1.25;
+ image = null;
+ sizeToImage(true);
+ return;
+ }
+ if (keypress.equals(kbAltDown)) {
+ // Make the image smaller.
+ scaleFactor *= 0.80;
+ image = null;
+ sizeToImage(true);
+ return;
+ }
+ if (keypress.equals(kbAltRight)) {
+ // Rotate clockwise.
+ clockwise++;
+ clockwise %= 4;
+ image = null;
+ sizeToImage(true);
+ return;
+ }
+ if (keypress.equals(kbAltLeft)) {
+ // Rotate counter-clockwise.
+ clockwise--;
+ if (clockwise < 0) {
+ clockwise = 3;
+ }
+ image = null;
+ sizeToImage(true);
+ return;
+ }
+
+ if (keypress.equals(kbShiftLeft)) {
+ switch (scale) {
+ case NONE:
+ setScaleType(Scale.SCALE);
+ return;
+ case STRETCH:
+ setScaleType(Scale.NONE);
+ return;
+ case SCALE:
+ setScaleType(Scale.STRETCH);
+ return;
+ }
+ }
+ if (keypress.equals(kbShiftRight)) {
+ switch (scale) {
+ case NONE:
+ setScaleType(Scale.STRETCH);
+ return;
+ case STRETCH:
+ setScaleType(Scale.SCALE);
+ return;
+ case SCALE:
+ setScaleType(Scale.NONE);
+ return;
+ }
+ }
+
+ // Pass to parent for the things we don't care about.
+ super.onKeypress(keypress);
+ }
+
+ /**
+ * Handle resize events.
+ *
+ * @param event resize event
+ */
+ @Override
+ public void onResize(final TResizeEvent event) {
+ // Get my width/height set correctly.
+ super.onResize(event);
+
+ if (scale == Scale.NONE) {
+ return;
+ }
+ image = null;
+ resized = true;
+ }
+
+ // ------------------------------------------------------------------------
+ // TWidget ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Draw the image.
+ */
+ @Override
+ public void draw() {
+ sizeToImage(false);
+
+ // We have already broken the image up, just draw the last set of
+ // cells.
+ for (int x = 0; (x < getWidth()) && (x + left < cellColumns); x++) {
+ if ((left + x) * lastTextWidth > image.getWidth()) {
+ continue;
+ }
+
+ for (int y = 0; (y < getHeight()) && (y + top < cellRows); y++) {
+ if ((top + y) * lastTextHeight > image.getHeight()) {
+ continue;
+ }
+ assert (x + left < cellColumns);
+ assert (y + top < cellRows);
+
+ getWindow().putCharXY(x, y, cells[x + left][y + top]);
+ }
+ }
+
+ }
+
+ // ------------------------------------------------------------------------
+ // TImage -----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Size cells[][] according to the screen font size.
+ *
+ * @param always if true, always resize the cells
+ */
+ private void sizeToImage(final boolean always) {
+ int textWidth = getScreen().getTextWidth();
+ int textHeight = getScreen().getTextHeight();
+
+ if (image == null) {
+ image = rotateImage(originalImage, clockwise);
+ image = scaleImage(image, scaleFactor, getWidth(), getHeight(),
+ textWidth, textHeight);
+ }
+
+ if ((always == true) ||
+ (resized == true) ||
+ ((textWidth > 0)
+ && (textWidth != lastTextWidth)
+ && (textHeight > 0)
+ && (textHeight != lastTextHeight))
+ ) {
+ resized = false;
+
+ cellColumns = image.getWidth() / textWidth;
+ if (cellColumns * textWidth < image.getWidth()) {
+ cellColumns++;
+ }
+ cellRows = image.getHeight() / textHeight;
+ if (cellRows * textHeight < image.getHeight()) {
+ cellRows++;
+ }
+
+ // Break the image up into an array of cells.
+ 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;
+ }
+ }
+
+ lastTextWidth = textWidth;
+ lastTextHeight = textHeight;
+ }
+
+ if ((left + getWidth()) > cellColumns) {
+ left = cellColumns - getWidth();
+ }
+ if (left < 0) {
+ left = 0;
+ }
+ if ((top + getHeight()) > cellRows) {
+ top = cellRows - getHeight();
+ }
+ if (top < 0) {
+ top = 0;
+ }
+ }
+
+ /**
+ * Get the top corner to render.
+ *
+ * @return the top row
+ */
+ public int getTop() {
+ return top;
+ }
+
+ /**
+ * Set the top corner to render.
+ *
+ * @param top the new top row
+ */
+ public void setTop(final int top) {
+ this.top = top;
+ if (this.top > cellRows - getHeight()) {
+ this.top = cellRows - getHeight();
+ }
+ if (this.top < 0) {
+ this.top = 0;
+ }
+ }
+
+ /**
+ * Get the left corner to render.
+ *
+ * @return the left column
+ */
+ public int getLeft() {
+ return left;
+ }
+
+ /**
+ * Set the left corner to render.
+ *
+ * @param left the new left column
+ */
+ public void setLeft(final int left) {
+ this.left = left;
+ if (this.left > cellColumns - getWidth()) {
+ this.left = cellColumns - getWidth();
+ }
+ if (this.left < 0) {
+ this.left = 0;
+ }
+ }
+
+ /**
+ * Get the number of text cell rows for this image.
+ *
+ * @return the number of rows
+ */
+ public int getRows() {
+ return cellRows;
+ }
+
+ /**
+ * Get the number of text cell columns for this image.
+ *
+ * @return the number of columns
+ */
+ public int getColumns() {
+ return cellColumns;
+ }
+
+ /**
+ * Get the raw (unprocessed) image.
+ *
+ * @return the image
+ */
+ public BufferedImage getImage() {
+ return originalImage;
+ }
+
+ /**
+ * Set the raw image, and reprocess to make the visible image.
+ *
+ * @param image the new image
+ */
+ public void setImage(final BufferedImage image) {
+ this.originalImage = image;
+ this.image = null;
+ sizeToImage(true);
+ }
+
+ /**
+ * Get the visible (processed) image.
+ *
+ * @return the image that is currently on screen
+ */
+ public BufferedImage getVisibleImage() {
+ return image;
+ }
+
+ /**
+ * Get the scaling strategy.
+ *
+ * @return Scale.NONE, Scale.STRETCH, etc.
+ */
+ public Scale getScaleType() {
+ return scale;
+ }
+
+ /**
+ * Set the scaling strategy.
+ *
+ * @param scale Scale.NONE, Scale.STRETCH, etc.
+ */
+ public void setScaleType(final Scale scale) {
+ this.scale = scale;
+ this.image = null;
+ sizeToImage(true);
+ }
+
+ /**
+ * Get the scale factor.
+ *
+ * @return the scale factor
+ */
+ public double getScaleFactor() {
+ return scaleFactor;
+ }
+
+ /**
+ * Set the scale factor. 1.0 means no scaling.
+ *
+ * @param scaleFactor the new scale factor
+ */
+ public void setScaleFactor(final double scaleFactor) {
+ this.scaleFactor = scaleFactor;
+ image = null;
+ sizeToImage(true);
+ }
+
+ /**
+ * Get the rotation, as degrees.
+ *
+ * @return the rotation in degrees
+ */
+ public int getRotation() {
+ switch (clockwise) {
+ case 0:
+ return 0;
+ case 1:
+ return 90;
+ case 2:
+ return 180;
+ case 3:
+ return 270;
+ default:
+ // Don't know how this happened, but fix it.
+ clockwise = 0;
+ image = null;
+ sizeToImage(true);
+ return 0;
+ }
+ }
+
+ /**
+ * Set the rotation, as degrees clockwise.
+ *
+ * @param rotation 0, 90, 180, or 270
+ */
+ public void setRotation(final int rotation) {
+ switch (rotation) {
+ case 0:
+ clockwise = 0;
+ break;
+ case 90:
+ clockwise = 1;
+ break;
+ case 180:
+ clockwise = 2;
+ break;
+ case 270:
+ clockwise = 3;
+ break;
+ default:
+ // Don't know how this happened, but fix it.
+ clockwise = 0;
+ break;
+ }
+
+ image = null;
+ sizeToImage(true);
+ }
+
+ /**
+ * Scale an image by to be scaleFactor size.
+ *
+ * @param image the image to scale
+ * @param factor the scale to make the new image
+ * @param width the number of text cell columns for the destination image
+ * @param height the number of text cell rows for the destination image
+ * @param textWidth the width in pixels for one text cell
+ * @param textHeight the height in pixels for one text cell
+ */
+ private BufferedImage scaleImage(final BufferedImage image,
+ final double factor, final int width, final int height,
+ final int textWidth, final int textHeight) {
+
+ if ((scale == Scale.NONE) && (Math.abs(factor - 1.0) < 0.03)) {
+ // If we are within 3% of 1.0, just return the original image.
+ return image;
+ }
+
+ int destWidth = 0;
+ int destHeight = 0;
+ int x = 0;
+ int y = 0;
+
+ BufferedImage newImage = null;
+
+ switch (scale) {
+ case NONE:
+ destWidth = (int) (image.getWidth() * factor);
+ destHeight = (int) (image.getHeight() * factor);
+ newImage = new BufferedImage(destWidth, destHeight,
+ BufferedImage.TYPE_INT_ARGB);
+ break;
+ case STRETCH:
+ destWidth = width * textWidth;
+ destHeight = height * textHeight;
+ newImage = new BufferedImage(destWidth, destHeight,
+ BufferedImage.TYPE_INT_ARGB);
+ break;
+ case SCALE:
+ double a = (double) image.getWidth() / image.getHeight();
+ double b = (double) (width * textWidth) / (height * textHeight);
+ assert (a > 0);
+ assert (b > 0);
+
+ /*
+ System.err.println("Scale: original " + image.getWidth() +
+ "x" + image.getHeight());
+ System.err.println(" screen " + (width * textWidth) +
+ "x" + (height * textHeight));
+ System.err.println("A " + a + " B " + b);
+ */
+
+ if (a > b) {
+ // Horizontal letterbox
+ destWidth = width * textWidth;
+ destHeight = (int) (destWidth / a);
+ y = ((height * textHeight) - destHeight) / 2;
+ assert (y >= 0);
+ /*
+ System.err.println("Horizontal letterbox: " + destWidth +
+ "x" + destHeight + ", Y offset " + y);
+ */
+ } else {
+ // Vertical letterbox
+ destHeight = height * textHeight;
+ destWidth = (int) (destHeight * a);
+ x = ((width * textWidth) - destWidth) / 2;
+ assert (x >= 0);
+ /*
+ System.err.println("Vertical letterbox: " + destWidth +
+ "x" + destHeight + ", X offset " + x);
+ */
+ }
+ newImage = new BufferedImage(width * textWidth, height * textHeight,
+ BufferedImage.TYPE_INT_ARGB);
+ break;
+ }
+
+ java.awt.Graphics gr = newImage.createGraphics();
+ if (scale == Scale.SCALE) {
+ gr.setColor(scaleBackColor);
+ gr.fillRect(0, 0, width * textWidth, height * textHeight);
+ }
+ gr.drawImage(image, x, y, destWidth, destHeight, null);
+ gr.dispose();
+ return newImage;
+ }
+
+ /**
+ * Rotate an image either clockwise or counterclockwise.
+ *
+ * @param image the image to scale
+ * @param clockwise number of turns clockwise
+ */
+ private BufferedImage rotateImage(final BufferedImage image,
+ final int clockwise) {
+
+ if (clockwise % 4 == 0) {
+ return image;
+ }
+
+ BufferedImage newImage = null;
+
+ if (clockwise % 4 == 1) {
+ // 90 degrees clockwise
+ newImage = new BufferedImage(image.getHeight(), image.getWidth(),
+ BufferedImage.TYPE_INT_ARGB);
+ for (int x = 0; x < image.getWidth(); x++) {
+ for (int y = 0; y < image.getHeight(); y++) {
+ newImage.setRGB(y, x,
+ image.getRGB(x, image.getHeight() - 1 - y));
+ }
+ }
+ } else if (clockwise % 4 == 2) {
+ // 180 degrees clockwise
+ newImage = new BufferedImage(image.getWidth(), image.getHeight(),
+ BufferedImage.TYPE_INT_ARGB);
+ for (int x = 0; x < image.getWidth(); x++) {
+ for (int y = 0; y < image.getHeight(); y++) {
+ newImage.setRGB(x, y,
+ image.getRGB(image.getWidth() - 1 - x,
+ image.getHeight() - 1 - y));
+ }
+ }
+ } else if (clockwise % 4 == 3) {
+ // 270 degrees clockwise
+ newImage = new BufferedImage(image.getHeight(), image.getWidth(),
+ BufferedImage.TYPE_INT_ARGB);
+ for (int x = 0; x < image.getWidth(); x++) {
+ for (int y = 0; y < image.getHeight(); y++) {
+ newImage.setRGB(y, x,
+ image.getRGB(image.getWidth() - 1 - x, y));
+ }
+ }
+ }
+
+ return newImage;
+ }
+
+ }
--- /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.awt.image.BufferedImage;
+ import java.io.File;
+ import java.io.IOException;
+ import java.util.ResourceBundle;
+ import javax.imageio.ImageIO;
+
+ import jexer.event.TKeypressEvent;
+ import jexer.event.TMouseEvent;
+ import jexer.event.TResizeEvent;
+ import static jexer.TKeypress.*;
+
+ /**
+ * TImageWindow shows an image with scrollbars.
+ */
+ public class TImageWindow extends TScrollableWindow {
+
+ /**
+ * Translated strings.
+ */
+ private static final ResourceBundle i18n = ResourceBundle.getBundle(TImageWindow.class.getName());
+
+ // ------------------------------------------------------------------------
+ // Constants --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The number of lines to scroll on mouse wheel up/down.
+ */
+ private static final int wheelScrollSize = 3;
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Hang onto the TImage so I can resize it with the window.
+ */
+ private TImage imageField;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor opens a file.
+ *
+ * @param parent the main application
+ * @param file the file to open
+ * @throws IOException if a java.io operation throws
+ */
+ public TImageWindow(final TApplication parent,
+ final File file) throws IOException {
+
+ this(parent, file, 0, 0, parent.getScreen().getWidth(),
+ parent.getDesktopBottom() - parent.getDesktopTop());
+ }
+
+ /**
+ * Public constructor opens a file.
+ *
+ * @param parent the main application
+ * @param file the file to open
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of window
+ * @param height height of window
+ * @throws IOException if a java.io operation throws
+ */
+ public TImageWindow(final TApplication parent, final File file,
+ final int x, final int y, final int width,
+ final int height) throws IOException {
+
+ super(parent, file.getName(), x, y, width, height, RESIZABLE);
+
+ BufferedImage image = ImageIO.read(file);
+
+ imageField = addImage(0, 0, getWidth() - 2, getHeight() - 2,
+ image, 0, 0);
+ setTitle(file.getName());
+
+ setupAfterImage();
+ }
+
+ /**
+ * Setup other fields after the image is created.
+ */
+ private void setupAfterImage() {
+ if (imageField.getRows() < getHeight() - 2) {
+ imageField.setHeight(imageField.getRows());
+ setHeight(imageField.getRows() + 2);
+ }
+ if (imageField.getColumns() < getWidth() - 2) {
+ imageField.setWidth(imageField.getColumns());
+ setWidth(imageField.getColumns() + 2);
+ }
+
+ hScroller = new THScroller(this,
+ Math.min(Math.max(0, getWidth() - 17), 17),
+ getHeight() - 2,
+ getWidth() - Math.min(Math.max(0, getWidth() - 17), 17) - 3);
+ vScroller = new TVScroller(this, getWidth() - 2, 0, getHeight() - 2);
+ setTopValue(0);
+ setBottomValue(imageField.getRows() - imageField.getHeight());
+ setLeftValue(0);
+ setRightValue(imageField.getColumns() - imageField.getWidth());
+
+ statusBar = newStatusBar(i18n.getString("statusBar"));
+ }
+
+ // ------------------------------------------------------------------------
+ // Event handlers ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Handle mouse press events.
+ *
+ * @param mouse mouse button press event
+ */
+ @Override
+ public void onMouseDown(final TMouseEvent mouse) {
+ // Use TWidget's code to pass the event to the children.
+ super.onMouseDown(mouse);
+
+ if (mouse.isMouseWheelUp()) {
+ imageField.setTop(imageField.getTop() - wheelScrollSize);
+ } else if (mouse.isMouseWheelDown()) {
+ imageField.setTop(imageField.getTop() + wheelScrollSize);
+ }
+ setVerticalValue(imageField.getTop());
+ }
+
+ /**
+ * Handle mouse release events.
+ *
+ * @param mouse mouse button release event
+ */
+ @Override
+ public void onMouseUp(final TMouseEvent mouse) {
+ // Use TWidget's code to pass the event to the children.
+ super.onMouseUp(mouse);
+
+ if (mouse.isMouse1() && mouseOnVerticalScroller(mouse)) {
+ // Clicked/dragged on vertical scrollbar
+ imageField.setTop(getVerticalValue());
+ }
+ if (mouse.isMouse1() && mouseOnHorizontalScroller(mouse)) {
+ // Clicked/dragged on horizontal scrollbar
+ imageField.setLeft(getHorizontalValue());
+ }
+ }
+
+ /**
+ * Method that subclasses can override to handle mouse movements.
+ *
+ * @param mouse mouse motion event
+ */
+ @Override
+ public void onMouseMotion(final TMouseEvent mouse) {
+ // Use TWidget's code to pass the event to the children.
+ super.onMouseMotion(mouse);
+
+ if (mouse.isMouse1() && mouseOnVerticalScroller(mouse)) {
+ // Clicked/dragged on vertical scrollbar
+ imageField.setTop(getVerticalValue());
+ }
+ if (mouse.isMouse1() && mouseOnHorizontalScroller(mouse)) {
+ // Clicked/dragged on horizontal scrollbar
+ imageField.setLeft(getHorizontalValue());
+ }
+ }
+
+ /**
+ * Handle window/screen resize events.
+ *
+ * @param event resize event
+ */
+ @Override
+ public void onResize(final TResizeEvent event) {
+ if (event.getType() == TResizeEvent.Type.WIDGET) {
+ // Resize the image field
+ TResizeEvent imageSize = new TResizeEvent(TResizeEvent.Type.WIDGET,
+ event.getWidth() - 2, event.getHeight() - 2);
+ imageField.onResize(imageSize);
+
+ // Have TScrollableWindow handle the scrollbars
+ super.onResize(event);
+ return;
+ }
+
+ // Pass to children instead
+ for (TWidget widget: getChildren()) {
+ widget.onResize(event);
+ }
+ }
+
+ /**
+ * Handle keystrokes.
+ *
+ * @param keypress keystroke event
+ */
+ @Override
+ public void onKeypress(final TKeypressEvent keypress) {
+ if (keypress.equals(kbUp)) {
+ verticalDecrement();
+ imageField.setTop(getVerticalValue());
+ return;
+ }
+ if (keypress.equals(kbDown)) {
+ verticalIncrement();
+ imageField.setTop(getVerticalValue());
+ return;
+ }
+ if (keypress.equals(kbPgUp)) {
+ bigVerticalDecrement();
+ imageField.setTop(getVerticalValue());
+ return;
+ }
+ if (keypress.equals(kbPgDn)) {
+ bigVerticalIncrement();
+ imageField.setTop(getVerticalValue());
+ return;
+ }
+ if (keypress.equals(kbRight)) {
+ horizontalIncrement();
+ imageField.setLeft(getHorizontalValue());
+ return;
+ }
+ if (keypress.equals(kbLeft)) {
+ horizontalDecrement();
+ imageField.setLeft(getHorizontalValue());
+ return;
+ }
+
+ // We did not take it, let the TImage instance see it.
+ super.onKeypress(keypress);
+
+ setVerticalValue(imageField.getTop());
+ setBottomValue(imageField.getRows() - imageField.getHeight());
+ setHorizontalValue(imageField.getLeft());
+ setRightValue(imageField.getColumns() - imageField.getWidth());
+ }
+
+ // ------------------------------------------------------------------------
+ // TWindow ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Draw the window.
+ */
+ @Override
+ public void draw() {
+ // Draw as normal.
+ super.draw();
+
+ // We have to get the scrollbar values after we have let the image
+ // try to draw.
+ setBottomValue(imageField.getRows() - imageField.getHeight());
+ setRightValue(imageField.getColumns() - imageField.getWidth());
+ }
+
+ }
--- /dev/null
+ statusBar=Alt-\u2190\u2192-Rotate Left/Right Alt-\u2191\u2193-Bigger/Smaller \u2190\u2192\u2191\u2193-Pan Shift-\u2190\u2192-Scale
--- /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;
+
+ /**
+ * TInputBox is a system-modal dialog with an OK button and a text input
+ * field. Call it like:
+ *
+ * <pre>
+ * {@code
+ * box = inputBox(title, caption);
+ * if (box.getText().equals("yes")) {
+ * ... the user entered "yes", do stuff ...
+ * }
+ * }
+ * </pre>
+ *
+ */
+ public class TInputBox extends TMessageBox {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The input field.
+ */
+ private TField field;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor. The input box will be centered on screen.
+ *
+ * @param application TApplication that manages this window
+ * @param title window title, will be centered along the top border
+ * @param caption message to display. Use embedded newlines to get a
+ * multi-line box.
+ */
+ public TInputBox(final TApplication application, final String title,
+ final String caption) {
+
+ this(application, title, caption, "", Type.OK);
+ }
+
+ /**
+ * Public constructor. The input box will be centered on screen.
+ *
+ * @param application TApplication that manages this window
+ * @param title window title, will be centered along the top border
+ * @param caption message to display. Use embedded newlines to get a
+ * multi-line box.
+ * @param text initial text to seed the field with
+ */
+ public TInputBox(final TApplication application, final String title,
+ final String caption, final String text) {
+
+ this(application, title, caption, text, Type.OK);
+ }
+
+ /**
+ * Public constructor. The input box will be centered on screen.
+ *
+ * @param application TApplication that manages this window
+ * @param title window title, will be centered along the top border
+ * @param caption message to display. Use embedded newlines to get a
+ * multi-line box.
+ * @param text initial text to seed the field with
+ * @param type one of the Type constants. Default is Type.OK.
+ */
+ public TInputBox(final TApplication application, final String title,
+ final String caption, final String text, final Type type) {
+
+ super(application, title, caption, type, false);
+
+ for (TWidget widget: getChildren()) {
+ if (widget instanceof TButton) {
+ widget.setY(widget.getY() + 2);
+ }
+ }
+
+ setHeight(getHeight() + 2);
+ field = addField(1, getHeight() - 6, getWidth() - 4, false, text);
+
+ // Set the secondaryThread to run me
+ getApplication().enableSecondaryEventReceiver(this);
+
+ // Yield to the secondary thread. When I come back from the
+ // constructor response will already be set.
+ getApplication().yield();
+ }
+
+ // ------------------------------------------------------------------------
+ // TMessageBox ------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ // ------------------------------------------------------------------------
+ // TInputBox --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Retrieve the answer text.
+ *
+ * @return the answer text
+ */
+ public String getText() {
+ return field.getText();
+ }
+
+ }
--- /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;
+
+ /**
+ * This class represents keystrokes.
+ */
+ public class TKeypress {
+
+ // ------------------------------------------------------------------------
+ // Constants --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ // Various special keystrokes
+
+ /**
+ * "No key".
+ */
+ public static final int NONE = 255;
+
+ /**
+ * Function key F1.
+ */
+ public static final int F1 = 1;
+
+ /**
+ * Function key F2.
+ */
+ public static final int F2 = 2;
+
+ /**
+ * Function key F3.
+ */
+ public static final int F3 = 3;
+
+ /**
+ * Function key F4.
+ */
+ public static final int F4 = 4;
+
+ /**
+ * Function key F5.
+ */
+ public static final int F5 = 5;
+
+ /**
+ * Function key F6.
+ */
+ public static final int F6 = 6;
+
+ /**
+ * Function key F7.
+ */
+ public static final int F7 = 7;
+
+ /**
+ * Function key F8.
+ */
+ public static final int F8 = 8;
+
+ /**
+ * Function key F9.
+ */
+ public static final int F9 = 9;
+
+ /**
+ * Function key F10.
+ */
+ public static final int F10 = 10;
+
+ /**
+ * Function key F11.
+ */
+ public static final int F11 = 11;
+
+ /**
+ * Function key F12.
+ */
+ public static final int F12 = 12;
+
+ /**
+ * Home.
+ */
+ public static final int HOME = 20;
+
+ /**
+ * End.
+ */
+ public static final int END = 21;
+
+ /**
+ * Page up.
+ */
+ public static final int PGUP = 22;
+
+ /**
+ * Page down.
+ */
+ public static final int PGDN = 23;
+
+ /**
+ * Insert.
+ */
+ public static final int INS = 24;
+
+ /**
+ * Delete.
+ */
+ public static final int DEL = 25;
+
+ /**
+ * Right arrow.
+ */
+ public static final int RIGHT = 30;
+
+ /**
+ * Left arrow.
+ */
+ public static final int LEFT = 31;
+
+ /**
+ * Up arrow.
+ */
+ public static final int UP = 32;
+
+ /**
+ * Down arrow.
+ */
+ public static final int DOWN = 33;
+
+ /**
+ * Tab.
+ */
+ public static final int TAB = 40;
+
+ /**
+ * Back-tab (shift-tab).
+ */
+ public static final int BTAB = 41;
+
+ /**
+ * Enter.
+ */
+ public static final int ENTER = 42;
+
+ /**
+ * Escape.
+ */
+ public static final int ESC = 43;
+
+ // Special "no-key" keypress, used to ignore undefined keystrokes
+ public static final TKeypress kbNoKey = new TKeypress(true,
+ TKeypress.NONE, ' ', false, false, false);
+
+ // Normal keys
+ public static final TKeypress kbF1 = new TKeypress(true,
+ TKeypress.F1, ' ', false, false, false);
+ public static final TKeypress kbF2 = new TKeypress(true,
+ TKeypress.F2, ' ', false, false, false);
+ public static final TKeypress kbF3 = new TKeypress(true,
+ TKeypress.F3, ' ', false, false, false);
+ public static final TKeypress kbF4 = new TKeypress(true,
+ TKeypress.F4, ' ', false, false, false);
+ public static final TKeypress kbF5 = new TKeypress(true,
+ TKeypress.F5, ' ', false, false, false);
+ public static final TKeypress kbF6 = new TKeypress(true,
+ TKeypress.F6, ' ', false, false, false);
+ public static final TKeypress kbF7 = new TKeypress(true,
+ TKeypress.F7, ' ', false, false, false);
+ public static final TKeypress kbF8 = new TKeypress(true,
+ TKeypress.F8, ' ', false, false, false);
+ public static final TKeypress kbF9 = new TKeypress(true,
+ TKeypress.F9, ' ', false, false, false);
+ public static final TKeypress kbF10 = new TKeypress(true,
+ TKeypress.F10, ' ', false, false, false);
+ public static final TKeypress kbF11 = new TKeypress(true,
+ TKeypress.F11, ' ', false, false, false);
+ public static final TKeypress kbF12 = new TKeypress(true,
+ TKeypress.F12, ' ', false, false, false);
+ public static final TKeypress kbAltF1 = new TKeypress(true,
+ TKeypress.F1, ' ', true, false, false);
+ public static final TKeypress kbAltF2 = new TKeypress(true,
+ TKeypress.F2, ' ', true, false, false);
+ public static final TKeypress kbAltF3 = new TKeypress(true,
+ TKeypress.F3, ' ', true, false, false);
+ public static final TKeypress kbAltF4 = new TKeypress(true,
+ TKeypress.F4, ' ', true, false, false);
+ public static final TKeypress kbAltF5 = new TKeypress(true,
+ TKeypress.F5, ' ', true, false, false);
+ public static final TKeypress kbAltF6 = new TKeypress(true,
+ TKeypress.F6, ' ', true, false, false);
+ public static final TKeypress kbAltF7 = new TKeypress(true,
+ TKeypress.F7, ' ', true, false, false);
+ public static final TKeypress kbAltF8 = new TKeypress(true,
+ TKeypress.F8, ' ', true, false, false);
+ public static final TKeypress kbAltF9 = new TKeypress(true,
+ TKeypress.F9, ' ', true, false, false);
+ public static final TKeypress kbAltF10 = new TKeypress(true,
+ TKeypress.F10, ' ', true, false, false);
+ public static final TKeypress kbAltF11 = new TKeypress(true,
+ TKeypress.F11, ' ', true, false, false);
+ public static final TKeypress kbAltF12 = new TKeypress(true,
+ TKeypress.F12, ' ', true, false, false);
+ public static final TKeypress kbCtrlF1 = new TKeypress(true,
+ TKeypress.F1, ' ', false, true, false);
+ public static final TKeypress kbCtrlF2 = new TKeypress(true,
+ TKeypress.F2, ' ', false, true, false);
+ public static final TKeypress kbCtrlF3 = new TKeypress(true,
+ TKeypress.F3, ' ', false, true, false);
+ public static final TKeypress kbCtrlF4 = new TKeypress(true,
+ TKeypress.F4, ' ', false, true, false);
+ public static final TKeypress kbCtrlF5 = new TKeypress(true,
+ TKeypress.F5, ' ', false, true, false);
+ public static final TKeypress kbCtrlF6 = new TKeypress(true,
+ TKeypress.F6, ' ', false, true, false);
+ public static final TKeypress kbCtrlF7 = new TKeypress(true,
+ TKeypress.F7, ' ', false, true, false);
+ public static final TKeypress kbCtrlF8 = new TKeypress(true,
+ TKeypress.F8, ' ', false, true, false);
+ public static final TKeypress kbCtrlF9 = new TKeypress(true,
+ TKeypress.F9, ' ', false, true, false);
+ public static final TKeypress kbCtrlF10 = new TKeypress(true,
+ TKeypress.F10, ' ', false, true, false);
+ public static final TKeypress kbCtrlF11 = new TKeypress(true,
+ TKeypress.F11, ' ', false, true, false);
+ public static final TKeypress kbCtrlF12 = new TKeypress(true,
+ TKeypress.F12, ' ', false, true, false);
+ public static final TKeypress kbShiftF1 = new TKeypress(true,
+ TKeypress.F1, ' ', false, false, true);
+ public static final TKeypress kbShiftF2 = new TKeypress(true,
+ TKeypress.F2, ' ', false, false, true);
+ public static final TKeypress kbShiftF3 = new TKeypress(true,
+ TKeypress.F3, ' ', false, false, true);
+ public static final TKeypress kbShiftF4 = new TKeypress(true,
+ TKeypress.F4, ' ', false, false, true);
+ public static final TKeypress kbShiftF5 = new TKeypress(true,
+ TKeypress.F5, ' ', false, false, true);
+ public static final TKeypress kbShiftF6 = new TKeypress(true,
+ TKeypress.F6, ' ', false, false, true);
+ public static final TKeypress kbShiftF7 = new TKeypress(true,
+ TKeypress.F7, ' ', false, false, true);
+ public static final TKeypress kbShiftF8 = new TKeypress(true,
+ TKeypress.F8, ' ', false, false, true);
+ public static final TKeypress kbShiftF9 = new TKeypress(true,
+ TKeypress.F9, ' ', false, false, true);
+ public static final TKeypress kbShiftF10 = new TKeypress(true,
+ TKeypress.F10, ' ', false, false, true);
+ public static final TKeypress kbShiftF11 = new TKeypress(true,
+ TKeypress.F11, ' ', false, false, true);
+ public static final TKeypress kbShiftF12 = new TKeypress(true,
+ TKeypress.F12, ' ', false, false, true);
+ public static final TKeypress kbEnter = new TKeypress(true,
+ TKeypress.ENTER, ' ', false, false, false);
+ public static final TKeypress kbTab = new TKeypress(true,
+ TKeypress.TAB, ' ', false, false, false);
+ public static final TKeypress kbEsc = new TKeypress(true,
+ TKeypress.ESC, ' ', false, false, false);
+ public static final TKeypress kbHome = new TKeypress(true,
+ TKeypress.HOME, ' ', false, false, false);
+ public static final TKeypress kbEnd = new TKeypress(true,
+ TKeypress.END, ' ', false, false, false);
+ public static final TKeypress kbPgUp = new TKeypress(true,
+ TKeypress.PGUP, ' ', false, false, false);
+ public static final TKeypress kbPgDn = new TKeypress(true,
+ TKeypress.PGDN, ' ', false, false, false);
+ public static final TKeypress kbIns = new TKeypress(true,
+ TKeypress.INS, ' ', false, false, false);
+ public static final TKeypress kbDel = new TKeypress(true,
+ TKeypress.DEL, ' ', false, false, false);
+ public static final TKeypress kbUp = new TKeypress(true,
+ TKeypress.UP, ' ', false, false, false);
+ public static final TKeypress kbDown = new TKeypress(true,
+ TKeypress.DOWN, ' ', false, false, false);
+ public static final TKeypress kbLeft = new TKeypress(true,
+ TKeypress.LEFT, ' ', false, false, false);
+ public static final TKeypress kbRight = new TKeypress(true,
+ TKeypress.RIGHT, ' ', false, false, false);
+ public static final TKeypress kbAltEnter = new TKeypress(true,
+ TKeypress.ENTER, ' ', true, false, false);
+ public static final TKeypress kbAltTab = new TKeypress(true,
+ TKeypress.TAB, ' ', true, false, false);
+ public static final TKeypress kbAltEsc = new TKeypress(true,
+ TKeypress.ESC, ' ', true, false, false);
+ public static final TKeypress kbAltHome = new TKeypress(true,
+ TKeypress.HOME, ' ', true, false, false);
+ public static final TKeypress kbAltEnd = new TKeypress(true,
+ TKeypress.END, ' ', true, false, false);
+ public static final TKeypress kbAltPgUp = new TKeypress(true,
+ TKeypress.PGUP, ' ', true, false, false);
+ public static final TKeypress kbAltPgDn = new TKeypress(true,
+ TKeypress.PGDN, ' ', true, false, false);
+ public static final TKeypress kbAltIns = new TKeypress(true,
+ TKeypress.INS, ' ', true, false, false);
+ public static final TKeypress kbAltDel = new TKeypress(true,
+ TKeypress.DEL, ' ', true, false, false);
+ public static final TKeypress kbAltUp = new TKeypress(true,
+ TKeypress.UP, ' ', true, false, false);
+ public static final TKeypress kbAltDown = new TKeypress(true,
+ TKeypress.DOWN, ' ', true, false, false);
+ public static final TKeypress kbAltLeft = new TKeypress(true,
+ TKeypress.LEFT, ' ', true, false, false);
+ public static final TKeypress kbAltRight = new TKeypress(true,
+ TKeypress.RIGHT, ' ', true, false, false);
+ public static final TKeypress kbCtrlEnter = new TKeypress(true,
+ TKeypress.ENTER, ' ', false, true, false);
+ public static final TKeypress kbCtrlTab = new TKeypress(true,
+ TKeypress.TAB, ' ', false, true, false);
+ public static final TKeypress kbCtrlEsc = new TKeypress(true,
+ TKeypress.ESC, ' ', false, true, false);
+ public static final TKeypress kbCtrlHome = new TKeypress(true,
+ TKeypress.HOME, ' ', false, true, false);
+ public static final TKeypress kbCtrlEnd = new TKeypress(true,
+ TKeypress.END, ' ', false, true, false);
+ public static final TKeypress kbCtrlPgUp = new TKeypress(true,
+ TKeypress.PGUP, ' ', false, true, false);
+ public static final TKeypress kbCtrlPgDn = new TKeypress(true,
+ TKeypress.PGDN, ' ', false, true, false);
+ public static final TKeypress kbCtrlIns = new TKeypress(true,
+ TKeypress.INS, ' ', false, true, false);
+ public static final TKeypress kbCtrlDel = new TKeypress(true,
+ TKeypress.DEL, ' ', false, true, false);
+ public static final TKeypress kbCtrlUp = new TKeypress(true,
+ TKeypress.UP, ' ', false, true, false);
+ public static final TKeypress kbCtrlDown = new TKeypress(true,
+ TKeypress.DOWN, ' ', false, true, false);
+ public static final TKeypress kbCtrlLeft = new TKeypress(true,
+ TKeypress.LEFT, ' ', false, true, false);
+ public static final TKeypress kbCtrlRight = new TKeypress(true,
+ TKeypress.RIGHT, ' ', false, true, false);
+ public static final TKeypress kbShiftEnter = new TKeypress(true,
+ TKeypress.ENTER, ' ', false, false, true);
+ public static final TKeypress kbShiftTab = new TKeypress(true,
+ TKeypress.TAB, ' ', false, false, true);
+ public static final TKeypress kbBackTab = new TKeypress(true,
+ TKeypress.BTAB, ' ', false, false, false);
+ public static final TKeypress kbShiftEsc = new TKeypress(true,
+ TKeypress.ESC, ' ', false, false, true);
+ public static final TKeypress kbShiftHome = new TKeypress(true,
+ TKeypress.HOME, ' ', false, false, true);
+ public static final TKeypress kbShiftEnd = new TKeypress(true,
+ TKeypress.END, ' ', false, false, true);
+ public static final TKeypress kbShiftPgUp = new TKeypress(true,
+ TKeypress.PGUP, ' ', false, false, true);
+ public static final TKeypress kbShiftPgDn = new TKeypress(true,
+ TKeypress.PGDN, ' ', false, false, true);
+ public static final TKeypress kbShiftIns = new TKeypress(true,
+ TKeypress.INS, ' ', false, false, true);
+ public static final TKeypress kbShiftDel = new TKeypress(true,
+ TKeypress.DEL, ' ', false, false, true);
+ public static final TKeypress kbShiftUp = new TKeypress(true,
+ TKeypress.UP, ' ', false, false, true);
+ public static final TKeypress kbShiftDown = new TKeypress(true,
+ TKeypress.DOWN, ' ', false, false, true);
+ public static final TKeypress kbShiftLeft = new TKeypress(true,
+ TKeypress.LEFT, ' ', false, false, true);
+ public static final TKeypress kbShiftRight = new TKeypress(true,
+ TKeypress.RIGHT, ' ', false, false, true);
+ public static final TKeypress kbA = new TKeypress(false,
+ 0, 'a', false, false, false);
+ public static final TKeypress kbB = new TKeypress(false,
+ 0, 'b', false, false, false);
+ public static final TKeypress kbC = new TKeypress(false,
+ 0, 'c', false, false, false);
+ public static final TKeypress kbD = new TKeypress(false,
+ 0, 'd', false, false, false);
+ public static final TKeypress kbE = new TKeypress(false,
+ 0, 'e', false, false, false);
+ public static final TKeypress kbF = new TKeypress(false,
+ 0, 'f', false, false, false);
+ public static final TKeypress kbG = new TKeypress(false,
+ 0, 'g', false, false, false);
+ public static final TKeypress kbH = new TKeypress(false,
+ 0, 'h', false, false, false);
+ public static final TKeypress kbI = new TKeypress(false,
+ 0, 'i', false, false, false);
+ public static final TKeypress kbJ = new TKeypress(false,
+ 0, 'j', false, false, false);
+ public static final TKeypress kbK = new TKeypress(false,
+ 0, 'k', false, false, false);
+ public static final TKeypress kbL = new TKeypress(false,
+ 0, 'l', false, false, false);
+ public static final TKeypress kbM = new TKeypress(false,
+ 0, 'm', false, false, false);
+ public static final TKeypress kbN = new TKeypress(false,
+ 0, 'n', false, false, false);
+ public static final TKeypress kbO = new TKeypress(false,
+ 0, 'o', false, false, false);
+ public static final TKeypress kbP = new TKeypress(false,
+ 0, 'p', false, false, false);
+ public static final TKeypress kbQ = new TKeypress(false,
+ 0, 'q', false, false, false);
+ public static final TKeypress kbR = new TKeypress(false,
+ 0, 'r', false, false, false);
+ public static final TKeypress kbS = new TKeypress(false,
+ 0, 's', false, false, false);
+ public static final TKeypress kbT = new TKeypress(false,
+ 0, 't', false, false, false);
+ public static final TKeypress kbU = new TKeypress(false,
+ 0, 'u', false, false, false);
+ public static final TKeypress kbV = new TKeypress(false,
+ 0, 'v', false, false, false);
+ public static final TKeypress kbW = new TKeypress(false,
+ 0, 'w', false, false, false);
+ public static final TKeypress kbX = new TKeypress(false,
+ 0, 'x', false, false, false);
+ public static final TKeypress kbY = new TKeypress(false,
+ 0, 'y', false, false, false);
+ public static final TKeypress kbZ = new TKeypress(false,
+ 0, 'z', false, false, false);
+ public static final TKeypress kbSpace = new TKeypress(false,
+ 0, ' ', false, false, false);
+ public static final TKeypress kbAltA = new TKeypress(false,
+ 0, 'a', true, false, false);
+ public static final TKeypress kbAltB = new TKeypress(false,
+ 0, 'b', true, false, false);
+ public static final TKeypress kbAltC = new TKeypress(false,
+ 0, 'c', true, false, false);
+ public static final TKeypress kbAltD = new TKeypress(false,
+ 0, 'd', true, false, false);
+ public static final TKeypress kbAltE = new TKeypress(false,
+ 0, 'e', true, false, false);
+ public static final TKeypress kbAltF = new TKeypress(false,
+ 0, 'f', true, false, false);
+ public static final TKeypress kbAltG = new TKeypress(false,
+ 0, 'g', true, false, false);
+ public static final TKeypress kbAltH = new TKeypress(false,
+ 0, 'h', true, false, false);
+ public static final TKeypress kbAltI = new TKeypress(false,
+ 0, 'i', true, false, false);
+ public static final TKeypress kbAltJ = new TKeypress(false,
+ 0, 'j', true, false, false);
+ public static final TKeypress kbAltK = new TKeypress(false,
+ 0, 'k', true, false, false);
+ public static final TKeypress kbAltL = new TKeypress(false,
+ 0, 'l', true, false, false);
+ public static final TKeypress kbAltM = new TKeypress(false,
+ 0, 'm', true, false, false);
+ public static final TKeypress kbAltN = new TKeypress(false,
+ 0, 'n', true, false, false);
+ public static final TKeypress kbAltO = new TKeypress(false,
+ 0, 'o', true, false, false);
+ public static final TKeypress kbAltP = new TKeypress(false,
+ 0, 'p', true, false, false);
+ public static final TKeypress kbAltQ = new TKeypress(false,
+ 0, 'q', true, false, false);
+ public static final TKeypress kbAltR = new TKeypress(false,
+ 0, 'r', true, false, false);
+ public static final TKeypress kbAltS = new TKeypress(false,
+ 0, 's', true, false, false);
+ public static final TKeypress kbAltT = new TKeypress(false,
+ 0, 't', true, false, false);
+ public static final TKeypress kbAltU = new TKeypress(false,
+ 0, 'u', true, false, false);
+ public static final TKeypress kbAltV = new TKeypress(false,
+ 0, 'v', true, false, false);
+ public static final TKeypress kbAltW = new TKeypress(false,
+ 0, 'w', true, false, false);
+ public static final TKeypress kbAltX = new TKeypress(false,
+ 0, 'x', true, false, false);
+ public static final TKeypress kbAltY = new TKeypress(false,
+ 0, 'y', true, false, false);
+ public static final TKeypress kbAltZ = new TKeypress(false,
+ 0, 'z', true, false, false);
+ public static final TKeypress kbAlt0 = new TKeypress(false,
+ 0, '0', true, false, false);
+ public static final TKeypress kbAlt1 = new TKeypress(false,
+ 0, '1', true, false, false);
+ public static final TKeypress kbAlt2 = new TKeypress(false,
+ 0, '2', true, false, false);
+ public static final TKeypress kbAlt3 = new TKeypress(false,
+ 0, '3', true, false, false);
+ public static final TKeypress kbAlt4 = new TKeypress(false,
+ 0, '4', true, false, false);
+ public static final TKeypress kbAlt5 = new TKeypress(false,
+ 0, '5', true, false, false);
+ public static final TKeypress kbAlt6 = new TKeypress(false,
+ 0, '6', true, false, false);
+ public static final TKeypress kbAlt7 = new TKeypress(false,
+ 0, '7', true, false, false);
+ public static final TKeypress kbAlt8 = new TKeypress(false,
+ 0, '8', true, false, false);
+ public static final TKeypress kbAlt9 = new TKeypress(false,
+ 0, '9', true, false, false);
+ public static final TKeypress kbCtrlA = new TKeypress(false,
+ 0, 'A', false, true, false);
+ public static final TKeypress kbCtrlB = new TKeypress(false,
+ 0, 'B', false, true, false);
+ public static final TKeypress kbCtrlC = new TKeypress(false,
+ 0, 'C', false, true, false);
+ public static final TKeypress kbCtrlD = new TKeypress(false,
+ 0, 'D', false, true, false);
+ public static final TKeypress kbCtrlE = new TKeypress(false,
+ 0, 'E', false, true, false);
+ public static final TKeypress kbCtrlF = new TKeypress(false,
+ 0, 'F', false, true, false);
+ public static final TKeypress kbCtrlG = new TKeypress(false,
+ 0, 'G', false, true, false);
+ public static final TKeypress kbCtrlH = new TKeypress(false,
+ 0, 'H', false, true, false);
+ public static final TKeypress kbCtrlI = new TKeypress(false,
+ 0, 'I', false, true, false);
+ public static final TKeypress kbCtrlJ = new TKeypress(false,
+ 0, 'J', false, true, false);
+ public static final TKeypress kbCtrlK = new TKeypress(false,
+ 0, 'K', false, true, false);
+ public static final TKeypress kbCtrlL = new TKeypress(false,
+ 0, 'L', false, true, false);
+ public static final TKeypress kbCtrlM = new TKeypress(false,
+ 0, 'M', false, true, false);
+ public static final TKeypress kbCtrlN = new TKeypress(false,
+ 0, 'N', false, true, false);
+ public static final TKeypress kbCtrlO = new TKeypress(false,
+ 0, 'O', false, true, false);
+ public static final TKeypress kbCtrlP = new TKeypress(false,
+ 0, 'P', false, true, false);
+ public static final TKeypress kbCtrlQ = new TKeypress(false,
+ 0, 'Q', false, true, false);
+ public static final TKeypress kbCtrlR = new TKeypress(false,
+ 0, 'R', false, true, false);
+ public static final TKeypress kbCtrlS = new TKeypress(false,
+ 0, 'S', false, true, false);
+ public static final TKeypress kbCtrlT = new TKeypress(false,
+ 0, 'T', false, true, false);
+ public static final TKeypress kbCtrlU = new TKeypress(false,
+ 0, 'U', false, true, false);
+ public static final TKeypress kbCtrlV = new TKeypress(false,
+ 0, 'V', false, true, false);
+ public static final TKeypress kbCtrlW = new TKeypress(false,
+ 0, 'W', false, true, false);
+ public static final TKeypress kbCtrlX = new TKeypress(false,
+ 0, 'X', false, true, false);
+ public static final TKeypress kbCtrlY = new TKeypress(false,
+ 0, 'Y', false, true, false);
+ public static final TKeypress kbCtrlZ = new TKeypress(false,
+ 0, 'Z', false, true, false);
+ public static final TKeypress kbAltShiftA = new TKeypress(false,
+ 0, 'A', true, false, true);
+ public static final TKeypress kbAltShiftB = new TKeypress(false,
+ 0, 'B', true, false, true);
+ public static final TKeypress kbAltShiftC = new TKeypress(false,
+ 0, 'C', true, false, true);
+ public static final TKeypress kbAltShiftD = new TKeypress(false,
+ 0, 'D', true, false, true);
+ public static final TKeypress kbAltShiftE = new TKeypress(false,
+ 0, 'E', true, false, true);
+ public static final TKeypress kbAltShiftF = new TKeypress(false,
+ 0, 'F', true, false, true);
+ public static final TKeypress kbAltShiftG = new TKeypress(false,
+ 0, 'G', true, false, true);
+ public static final TKeypress kbAltShiftH = new TKeypress(false,
+ 0, 'H', true, false, true);
+ public static final TKeypress kbAltShiftI = new TKeypress(false,
+ 0, 'I', true, false, true);
+ public static final TKeypress kbAltShiftJ = new TKeypress(false,
+ 0, 'J', true, false, true);
+ public static final TKeypress kbAltShiftK = new TKeypress(false,
+ 0, 'K', true, false, true);
+ public static final TKeypress kbAltShiftL = new TKeypress(false,
+ 0, 'L', true, false, true);
+ public static final TKeypress kbAltShiftM = new TKeypress(false,
+ 0, 'M', true, false, true);
+ public static final TKeypress kbAltShiftN = new TKeypress(false,
+ 0, 'N', true, false, true);
+ public static final TKeypress kbAltShiftO = new TKeypress(false,
+ 0, 'O', true, false, true);
+ public static final TKeypress kbAltShiftP = new TKeypress(false,
+ 0, 'P', true, false, true);
+ public static final TKeypress kbAltShiftQ = new TKeypress(false,
+ 0, 'Q', true, false, true);
+ public static final TKeypress kbAltShiftR = new TKeypress(false,
+ 0, 'R', true, false, true);
+ public static final TKeypress kbAltShiftS = new TKeypress(false,
+ 0, 'S', true, false, true);
+ public static final TKeypress kbAltShiftT = new TKeypress(false,
+ 0, 'T', true, false, true);
+ public static final TKeypress kbAltShiftU = new TKeypress(false,
+ 0, 'U', true, false, true);
+ public static final TKeypress kbAltShiftV = new TKeypress(false,
+ 0, 'V', true, false, true);
+ public static final TKeypress kbAltShiftW = new TKeypress(false,
+ 0, 'W', true, false, true);
+ public static final TKeypress kbAltShiftX = new TKeypress(false,
+ 0, 'X', true, false, true);
+ public static final TKeypress kbAltShiftY = new TKeypress(false,
+ 0, 'Y', true, false, true);
+ public static final TKeypress kbAltShiftZ = new TKeypress(false,
+ 0, 'Z', true, false, true);
+
+ /**
+ * Backspace as ^H.
+ */
+ public static final TKeypress kbBackspace = new TKeypress(false,
+ 0, 'H', false, true, false);
+
+ /**
+ * Backspace as ^?.
+ */
+ public static final TKeypress kbBackspaceDel = new TKeypress(false,
+ 0, (char) 0x7F, false, false, false);
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * If true, ch is meaningless, use keyCode instead.
+ */
+ private boolean isFunctionKey;
+
+ /**
+ * Will be set to F1, F2, HOME, END, etc. if isKey is true.
+ */
+ private int keyCode;
+
+ /**
+ * Keystroke modifier ALT.
+ */
+ private boolean alt;
+
+ /**
+ * Keystroke modifier CTRL.
+ */
+ private boolean ctrl;
+
+ /**
+ * Keystroke modifier SHIFT.
+ */
+ private boolean shift;
+
+ /**
+ * The character received.
+ */
+ private int ch;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor makes an immutable instance.
+ *
+ * @param isKey is true, this is a function key
+ * @param fnKey the function key code (only valid if isKey is true)
+ * @param ch the character (only valid if fnKey is false)
+ * @param alt if true, ALT was pressed with this keystroke
+ * @param ctrl if true, CTRL was pressed with this keystroke
+ * @param shift if true, SHIFT was pressed with this keystroke
+ */
+ public TKeypress(final boolean isKey, final int fnKey, final int ch,
+ final boolean alt, final boolean ctrl, final boolean shift) {
+
+ this.isFunctionKey = isKey;
+ this.keyCode = fnKey;
+ this.ch = ch;
+ this.alt = alt;
+ this.ctrl = ctrl;
+ this.shift = shift;
+ }
+
+ // ------------------------------------------------------------------------
+ // TKeypress --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Getter for isFunctionKey.
+ *
+ * @return if true, ch is meaningless, use keyCode instead
+ */
+ public boolean isFnKey() {
+ return isFunctionKey;
+ }
+
+ /**
+ * Getter for function key code.
+ *
+ * @return function key code int value (only valid is isKey is true)
+ */
+ public int getKeyCode() {
+ return keyCode;
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Getter for character.
+ *
+ * @return the character (only valid if isKey is false)
+ */
+ public int getChar() {
+ return ch;
+ }
+
+ /**
+ * Create a duplicate instance.
+ *
+ * @return duplicate intance
+ */
+ public TKeypress dup() {
+ TKeypress keypress = new TKeypress(isFunctionKey, keyCode, ch,
+ alt, ctrl, shift);
+ return keypress;
+ }
+
+ /**
+ * Comparison check. All fields must match to return true.
+ *
+ * @param rhs another TKeypress instance
+ * @return true if all fields are equal
+ */
+ @Override
+ public boolean equals(final Object rhs) {
+ if (!(rhs instanceof TKeypress)) {
+ return false;
+ }
+
+ TKeypress that = (TKeypress) rhs;
+ return ((isFunctionKey == that.isFunctionKey)
+ && (keyCode == that.keyCode)
+ && (ch == that.ch)
+ && (alt == that.alt)
+ && (ctrl == that.ctrl)
+ && (shift == that.shift));
+ }
+
+ /**
+ * Comparison check, omitting the ctrl/alt/shift flags.
+ *
+ * @param rhs another TKeypress instance
+ * @return true if all fields (except for ctrl/alt/shift) are equal
+ */
+ public boolean equalsWithoutModifiers(final Object rhs) {
+ if (!(rhs instanceof TKeypress)) {
+ return false;
+ }
+
+ TKeypress that = (TKeypress) rhs;
+ return ((isFunctionKey == that.isFunctionKey)
+ && (keyCode == that.keyCode)
+ && (ch == that.ch));
+ }
+
+ /**
+ * Hashcode uses all fields in equals().
+ *
+ * @return the hash
+ */
+ @Override
+ public int hashCode() {
+ int A = 13;
+ int B = 23;
+ int hash = A;
+ hash = (B * hash) + (isFunctionKey ? 1 : 0);
+ hash = (B * hash) + keyCode;
+ hash = (B * hash) + ch;
+ hash = (B * hash) + (alt ? 1 : 0);
+ hash = (B * hash) + (ctrl ? 1 : 0);
+ hash = (B * hash) + (shift ? 1 : 0);
+ return hash;
+ }
+
+ /**
+ * Make human-readable description of this TKeypress.
+ *
+ * @return displayable String
+ */
+ @Override
+ public String toString() {
+ // Special case: Enter is "<arrow> <line> <angle>"
+ if (equals(kbEnter)) {
+ return "\u25C0\u2500\u2518";
+ }
+
+ if (equals(kbShiftLeft)) {
+ return "Shift+\u2190";
+ }
+ if (equals(kbShiftRight)) {
+ return "Shift+\u2192";
+ }
+
+ if (isFunctionKey) {
+ switch (keyCode) {
+ case F1:
+ return String.format("%s%s%sF1",
+ ctrl ? "Ctrl+" : "",
+ alt ? "Alt+" : "",
+ shift ? "Shift+" : "");
+ case F2:
+ return String.format("%s%s%sF2",
+ ctrl ? "Ctrl+" : "",
+ alt ? "Alt+" : "",
+ shift ? "Shift+" : "");
+ case F3:
+ return String.format("%s%s%sF3",
+ ctrl ? "Ctrl+" : "",
+ alt ? "Alt+" : "",
+ shift ? "Shift+" : "");
+ case F4:
+ return String.format("%s%s%sF4",
+ ctrl ? "Ctrl+" : "",
+ alt ? "Alt+" : "",
+ shift ? "Shift+" : "");
+ case F5:
+ return String.format("%s%s%sF5",
+ ctrl ? "Ctrl+" : "",
+ alt ? "Alt+" : "",
+ shift ? "Shift+" : "");
+ case F6:
+ return String.format("%s%s%sF6",
+ ctrl ? "Ctrl+" : "",
+ alt ? "Alt+" : "",
+ shift ? "Shift+" : "");
+ case F7:
+ return String.format("%s%s%sF7",
+ ctrl ? "Ctrl+" : "",
+ alt ? "Alt+" : "",
+ shift ? "Shift+" : "");
+ case F8:
+ return String.format("%s%s%sF8",
+ ctrl ? "Ctrl+" : "",
+ alt ? "Alt+" : "",
+ shift ? "Shift+" : "");
+ case F9:
+ return String.format("%s%s%sF9",
+ ctrl ? "Ctrl+" : "",
+ alt ? "Alt+" : "",
+ shift ? "Shift+" : "");
+ case F10:
+ return String.format("%s%s%sF10",
+ ctrl ? "Ctrl+" : "",
+ alt ? "Alt+" : "",
+ shift ? "Shift+" : "");
+ case F11:
+ return String.format("%s%s%sF11",
+ ctrl ? "Ctrl+" : "",
+ alt ? "Alt+" : "",
+ shift ? "Shift+" : "");
+ case F12:
+ return String.format("%s%s%sF12",
+ ctrl ? "Ctrl+" : "",
+ alt ? "Alt+" : "",
+ shift ? "Shift+" : "");
+ case HOME:
+ return String.format("%s%s%sHOME",
+ ctrl ? "Ctrl+" : "",
+ alt ? "Alt+" : "",
+ shift ? "Shift+" : "");
+ case END:
+ return String.format("%s%s%sEND",
+ ctrl ? "Ctrl+" : "",
+ alt ? "Alt+" : "",
+ shift ? "Shift+" : "");
+ case PGUP:
+ return String.format("%s%s%sPGUP",
+ ctrl ? "Ctrl+" : "",
+ alt ? "Alt+" : "",
+ shift ? "Shift+" : "");
+ case PGDN:
+ return String.format("%s%s%sPGDN",
+ ctrl ? "Ctrl+" : "",
+ alt ? "Alt+" : "",
+ shift ? "Shift+" : "");
+ case INS:
+ return String.format("%s%s%sINS",
+ ctrl ? "Ctrl+" : "",
+ alt ? "Alt+" : "",
+ shift ? "Shift+" : "");
+ case DEL:
+ return String.format("%s%s%sDEL",
+ ctrl ? "Ctrl+" : "",
+ alt ? "Alt+" : "",
+ shift ? "Shift+" : "");
+ case RIGHT:
+ return String.format("%s%s%sRIGHT",
+ ctrl ? "Ctrl+" : "",
+ alt ? "Alt+" : "",
+ shift ? "Shift+" : "");
+ case LEFT:
+ return String.format("%s%s%sLEFT",
+ ctrl ? "Ctrl+" : "",
+ alt ? "Alt+" : "",
+ shift ? "Shift+" : "");
+ case UP:
+ return String.format("%s%s%sUP",
+ ctrl ? "Ctrl+" : "",
+ alt ? "Alt+" : "",
+ shift ? "Shift+" : "");
+ case DOWN:
+ return String.format("%s%s%sDOWN",
+ ctrl ? "Ctrl+" : "",
+ alt ? "Alt+" : "",
+ shift ? "Shift+" : "");
+ case TAB:
+ return String.format("%s%s%sTAB",
+ ctrl ? "Ctrl+" : "",
+ alt ? "Alt+" : "",
+ shift ? "Shift+" : "");
+ case BTAB:
+ return String.format("%s%s%sBTAB",
+ ctrl ? "Ctrl+" : "",
+ alt ? "Alt+" : "",
+ shift ? "Shift+" : "");
+ case ENTER:
+ return String.format("%s%s%sENTER",
+ ctrl ? "Ctrl+" : "",
+ alt ? "Alt+" : "",
+ shift ? "Shift+" : "");
+ case ESC:
+ return String.format("%s%s%sESC",
+ ctrl ? "Ctrl+" : "",
+ alt ? "Alt+" : "",
+ shift ? "Shift+" : "");
+ default:
+ return String.format("--UNKNOWN--");
+ }
+ } else {
+ if (alt && !shift && !ctrl) {
+ // Alt-X
+ return String.format("Alt+%c", Character.toUpperCase(ch));
+ } else if (!alt && shift && !ctrl) {
+ // Shift-X
+ return String.format("%c", ch);
+ } else if (!alt && !shift && ctrl) {
+ // Ctrl-X
+ return String.format("Ctrl+%c", ch);
+ } else if (alt && shift && !ctrl) {
+ // Alt-Shift-X
+ return String.format("Alt+Shift+%c", ch);
+ } else if (!alt && shift && ctrl) {
+ // Ctrl-Shift-X
+ return String.format("Ctrl+Shift+%c", ch);
+ } else if (alt && !shift && ctrl) {
+ // Ctrl-Alt-X
+ return String.format("Ctrl+Alt+%c", Character.toUpperCase(ch));
+ } else if (alt && shift && ctrl) {
+ // Ctrl-Alt-Shift-X
+ return String.format("Ctrl+Alt+Shift+%c",
+ Character.toUpperCase(ch));
+ } else {
+ // X
+ return String.format("%c", ch);
+ }
+ }
+ }
+
+ /**
+ * Convert a keypress to lowercase. Function keys and alt/ctrl keys are
+ * not converted.
+ *
+ * @return a new instance with the key converted
+ */
+ public TKeypress toLowerCase() {
+ TKeypress newKey = new TKeypress(isFunctionKey, keyCode, ch, alt, ctrl,
+ shift);
+ if (!isFunctionKey && (ch >= 'A') && (ch <= 'Z') && !ctrl && !alt) {
+ newKey.shift = false;
+ newKey.ch += 32;
+ }
+ return newKey;
+ }
+
+ /**
+ * Convert a keypress to uppercase. Function keys and alt/ctrl keys are
+ * not converted.
+ *
+ * @return a new instance with the key converted
+ */
+ public TKeypress toUpperCase() {
+ TKeypress newKey = new TKeypress(isFunctionKey, keyCode, ch, alt, ctrl,
+ shift);
+ if (!isFunctionKey && (ch >= 'a') && (ch <= 'z') && !ctrl && !alt) {
+ newKey.shift = true;
+ newKey.ch -= 32;
+ }
+ return newKey;
+ }
+
+ }
--- /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 jexer.bits.CellAttributes;
+ import jexer.bits.MnemonicString;
+ import jexer.bits.StringUtils;
+
+ /**
+ * TLabel implements a simple label, with an optional mnemonic hotkey action
+ * associated with it.
+ */
+ public class TLabel extends TWidget {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The shortcut and label.
+ */
+ private MnemonicString mnemonic;
+
+ /**
+ * The action to perform when the mnemonic shortcut is pressed.
+ */
+ private TAction action;
+
+ /**
+ * Label color.
+ */
+ private String colorKey;
+
+ /**
+ * If true, use the window's background color.
+ */
+ private boolean useWindowBackground = true;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor, using the default "tlabel" for colorKey.
+ *
+ * @param parent parent widget
+ * @param text label on the screen
+ * @param x column relative to parent
+ * @param y row relative to parent
+ */
+ public TLabel(final TWidget parent, final String text, final int x,
+ final int y) {
+
+ this(parent, text, x, y, "tlabel");
+ }
+
+ /**
+ * Public constructor, using the default "tlabel" for colorKey.
+ *
+ * @param parent parent widget
+ * @param text label on the screen
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param action to call when shortcut is pressed
+ */
+ public TLabel(final TWidget parent, final String text, final int x,
+ final int y, final TAction action) {
+
+ this(parent, text, x, y, "tlabel", action);
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param text label on the screen
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param colorKey ColorTheme key color to use for foreground text
+ */
+ public TLabel(final TWidget parent, final String text, final int x,
+ final int y, final String colorKey) {
+
+ this(parent, text, x, y, colorKey, true);
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param text label on the screen
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param colorKey ColorTheme key color to use for foreground text
+ * @param action to call when shortcut is pressed
+ */
+ public TLabel(final TWidget parent, final String text, final int x,
+ final int y, final String colorKey, final TAction action) {
+
+ this(parent, text, x, y, colorKey, true, action);
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param text label on the screen
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param colorKey ColorTheme key color to use for foreground text
+ * @param useWindowBackground if true, use the window's background color
+ */
+ public TLabel(final TWidget parent, final String text, final int x,
+ final int y, final String colorKey, final boolean useWindowBackground) {
+
+ this(parent, text, x, y, colorKey, useWindowBackground, null);
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param text label on the screen
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param colorKey ColorTheme key color to use for foreground text
+ * @param useWindowBackground if true, use the window's background color
+ * @param action to call when shortcut is pressed
+ */
+ public TLabel(final TWidget parent, final String text, final int x,
+ final int y, final String colorKey, final boolean useWindowBackground,
+ final TAction action) {
+
+ // Set parent and window
+ super(parent, false, x, y, 0, 1);
+
+ setLabel(text);
+ this.colorKey = colorKey;
+ this.useWindowBackground = useWindowBackground;
+ this.action = action;
+ }
+
+ // ------------------------------------------------------------------------
+ // TWidget ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Override TWidget's width: we can only set width at construction time.
+ *
+ * @param width new widget width (ignored)
+ */
+ @Override
+ public void setWidth(final int width) {
+ // Do nothing
+ }
+
+ /**
+ * Override TWidget's height: we can only set height at construction
+ * time.
+ *
+ * @param height new widget height (ignored)
+ */
+ @Override
+ public void setHeight(final int height) {
+ // Do nothing
+ }
+
+ /**
+ * Draw a static label.
+ */
+ @Override
+ public void draw() {
+ // Setup my color
+ CellAttributes color = new CellAttributes();
+ CellAttributes mnemonicColor = new CellAttributes();
+ color.setTo(getTheme().getColor(colorKey));
+ mnemonicColor.setTo(getTheme().getColor("tlabel.mnemonic"));
+ if (useWindowBackground) {
+ CellAttributes background = getWindow().getBackground();
+ color.setBackColor(background.getBackColor());
+ mnemonicColor.setBackColor(background.getBackColor());
+ }
+ putStringXY(0, 0, mnemonic.getRawLabel(), color);
+ if (mnemonic.getScreenShortcutIdx() >= 0) {
+ putCharXY(mnemonic.getScreenShortcutIdx(), 0,
+ mnemonic.getShortcut(), mnemonicColor);
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // TLabel -----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get label raw text.
+ *
+ * @return label text
+ */
+ public String getLabel() {
+ return mnemonic.getRawLabel();
+ }
+
+ /**
+ * Get the mnemonic string for this label.
+ *
+ * @return mnemonic string
+ */
+ public MnemonicString getMnemonic() {
+ return mnemonic;
+ }
+
+ /**
+ * Set label text.
+ *
+ * @param label new label text
+ */
+ public void setLabel(final String label) {
+ mnemonic = new MnemonicString(label);
+ super.setWidth(StringUtils.width(mnemonic.getRawLabel()));
+ }
+
+ /**
+ * Get the label color.
+ *
+ * @return the ColorTheme key color to use for foreground text
+ */
+ public String getColorKey() {
+ return colorKey;
+ }
+
+ /**
+ * Set the label color.
+ *
+ * @param colorKey ColorTheme key color to use for foreground text
+ */
+ public void setColorKey(final String colorKey) {
+ this.colorKey = colorKey;
+ }
+
+ /**
+ * Act as though the mnemonic shortcut was pressed.
+ */
+ public void dispatch() {
+ if (action != null) {
+ action.DO(this);
+ }
+ }
+
+ }
--- /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.ArrayList;
+ import java.util.List;
+
+ import jexer.bits.CellAttributes;
+ import jexer.bits.StringUtils;
+ import jexer.event.TKeypressEvent;
+ import jexer.event.TMouseEvent;
+ import static jexer.TKeypress.*;
+
+ /**
+ * TList shows a list of strings, and lets the user select one.
+ */
+ public class TList extends TScrollableWidget {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The list of strings to display.
+ */
+ private List<String> strings;
+
+ /**
+ * Selected string.
+ */
+ private int selectedString = -1;
+
+ /**
+ * Maximum width of a single line.
+ */
+ private int maxLineWidth;
+
+ /**
+ * The action to perform when the user selects an item (double-clicks or
+ * enter).
+ */
+ protected TAction enterAction = null;
+
+ /**
+ * The action to perform when the user selects an item (single-click).
+ */
+ protected TAction singleClickAction = null;
+
+ /**
+ * The action to perform when the user navigates with keyboard.
+ */
+ protected TAction moveAction = null;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param strings list of strings to show
+ * @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 TList(final TWidget parent, final List<String> strings, final int x,
+ final int y, final int width, final int height) {
+
+ this(parent, strings, x, y, width, height, null, null, null);
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param strings list of strings to show. This is allowed to be null
+ * and set later with setList() or by subclasses.
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of text area
+ * @param height height of text area
+ * @param enterAction action to perform when an item is selected
+ */
+ public TList(final TWidget parent, final List<String> strings, final int x,
+ final int y, final int width, final int height,
+ final TAction enterAction) {
+
+ this(parent, strings, x, y, width, height, enterAction, null, null);
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param strings list of strings to show. This is allowed to be null
+ * and set later with setList() or by subclasses.
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of text area
+ * @param height height of text area
+ * @param enterAction action to perform when an item is selected
+ * @param moveAction action to perform when the user navigates to a new
+ * item with arrow/page keys
+ */
+ public TList(final TWidget parent, final List<String> strings, final int x,
+ final int y, final int width, final int height,
+ final TAction enterAction, final TAction moveAction) {
+
+ this(parent, strings, x, y, width, height, enterAction, moveAction,
+ null);
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param strings list of strings to show. This is allowed to be null
+ * and set later with setList() or by subclasses.
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of text area
+ * @param height height of text area
+ * @param enterAction action to perform when an item is selected
+ * @param moveAction action to perform when the user navigates to a new
+ * item with arrow/page keys
+ * @param singleClickAction action to perform when the user clicks on an
+ * item
+ */
+ public TList(final TWidget parent, final List<String> strings, final int x,
+ final int y, final int width, final int height,
+ final TAction enterAction, final TAction moveAction,
+ final TAction singleClickAction) {
+
+ super(parent, x, y, width, height);
+ this.enterAction = enterAction;
+ this.moveAction = moveAction;
+ this.singleClickAction = singleClickAction;
+ this.strings = new ArrayList<String>();
+ if (strings != null) {
+ this.strings.addAll(strings);
+ }
+
+ hScroller = new THScroller(this, 0, getHeight() - 1, getWidth() - 1);
+ vScroller = new TVScroller(this, getWidth() - 1, 0, getHeight() - 1);
+ reflowData();
+ }
+
+ // ------------------------------------------------------------------------
+ // Event handlers ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Handle mouse press events.
+ *
+ * @param mouse mouse button press event
+ */
+ @Override
+ public void onMouseDown(final TMouseEvent mouse) {
+ if (mouse.isMouseWheelUp()) {
+ verticalDecrement();
+ return;
+ }
+ if (mouse.isMouseWheelDown()) {
+ verticalIncrement();
+ return;
+ }
+
+ if ((mouse.getX() < getWidth() - 1)
+ && (mouse.getY() < getHeight() - 1)
+ ) {
+ if (getVerticalValue() + mouse.getY() < strings.size()) {
+ selectedString = getVerticalValue() + mouse.getY();
+ dispatchSingleClick();
+ }
+ return;
+ }
+
+ // Pass to children
+ super.onMouseDown(mouse);
+ }
+
+ /**
+ * Handle mouse double click.
+ *
+ * @param mouse mouse double click event
+ */
+ @Override
+ public void onMouseDoubleClick(final TMouseEvent mouse) {
+ if ((mouse.getX() < getWidth() - 1)
+ && (mouse.getY() < getHeight() - 1)
+ ) {
+ if (getVerticalValue() + mouse.getY() < strings.size()) {
+ selectedString = getVerticalValue() + mouse.getY();
+ dispatchEnter();
+ }
+ return;
+ }
+
+ // Pass to children
+ super.onMouseDoubleClick(mouse);
+ }
+
+ /**
+ * Handle keystrokes.
+ *
+ * @param keypress keystroke event
+ */
+ @Override
+ public void onKeypress(final TKeypressEvent keypress) {
+ if (keypress.equals(kbLeft)) {
+ horizontalDecrement();
+ } else if (keypress.equals(kbRight)) {
+ horizontalIncrement();
+ } else if (keypress.equals(kbUp)) {
+ if (strings.size() > 0) {
+ if (selectedString >= 0) {
+ if (selectedString > 0) {
+ if (selectedString - getVerticalValue() == 0) {
+ verticalDecrement();
+ }
+ selectedString--;
+ }
+ } else {
+ selectedString = strings.size() - 1;
+ }
+ }
+ if (selectedString >= 0) {
+ dispatchMove();
+ }
+ } else if (keypress.equals(kbDown)) {
+ if (strings.size() > 0) {
+ if (selectedString >= 0) {
+ if (selectedString < strings.size() - 1) {
+ selectedString++;
+ if (selectedString - getVerticalValue() == getHeight() - 1) {
+ verticalIncrement();
+ }
+ }
+ } else {
+ selectedString = 0;
+ }
+ }
+ if (selectedString >= 0) {
+ dispatchMove();
+ }
+ } else if (keypress.equals(kbPgUp)) {
+ bigVerticalDecrement();
+ if (selectedString >= 0) {
+ selectedString -= getHeight() - 1;
+ if (selectedString < 0) {
+ selectedString = 0;
+ }
+ }
+ if (selectedString >= 0) {
+ dispatchMove();
+ }
+ } else if (keypress.equals(kbPgDn)) {
+ bigVerticalIncrement();
+ if (selectedString >= 0) {
+ selectedString += getHeight() - 1;
+ if (selectedString > strings.size() - 1) {
+ selectedString = strings.size() - 1;
+ }
+ }
+ if (selectedString >= 0) {
+ dispatchMove();
+ }
+ } else if (keypress.equals(kbHome)) {
+ toTop();
+ if (strings.size() > 0) {
+ selectedString = 0;
+ }
+ if (selectedString >= 0) {
+ dispatchMove();
+ }
+ } else if (keypress.equals(kbEnd)) {
+ toBottom();
+ if (strings.size() > 0) {
+ selectedString = strings.size() - 1;
+ }
+ if (selectedString >= 0) {
+ dispatchMove();
+ }
+ } else if (keypress.equals(kbTab)) {
+ getParent().switchWidget(true);
+ } else if (keypress.equals(kbShiftTab) || keypress.equals(kbBackTab)) {
+ getParent().switchWidget(false);
+ } else if (keypress.equals(kbEnter)) {
+ if (selectedString >= 0) {
+ dispatchEnter();
+ }
+ } else {
+ // Pass other keys (tab etc.) on
+ super.onKeypress(keypress);
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // 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);
+ hScroller.setWidth(getWidth() - 1);
+ 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);
+ }
+
+ /**
+ * Resize for a new width/height.
+ */
+ @Override
+ public void reflowData() {
+
+ // Reset the lines
+ selectedString = -1;
+ maxLineWidth = 0;
+
+ for (int i = 0; i < strings.size(); i++) {
+ String line = strings.get(i);
+ int lineLength = StringUtils.width(line);
+ if (lineLength > maxLineWidth) {
+ maxLineWidth = lineLength;
+ }
+ }
+
+ setBottomValue(strings.size() - getHeight() + 1);
+ if (getBottomValue() < 0) {
+ setBottomValue(0);
+ }
+
+ setRightValue(maxLineWidth - getWidth() + 1);
+ if (getRightValue() < 0) {
+ setRightValue(0);
+ }
+ }
+
+ /**
+ * Draw the list.
+ */
+ @Override
+ public void draw() {
+ CellAttributes color = null;
+ int begin = getVerticalValue();
+ int topY = 0;
+ for (int i = begin; i < strings.size(); i++) {
+ String line = strings.get(i);
+ if (getHorizontalValue() < line.length()) {
+ line = line.substring(getHorizontalValue());
+ } else {
+ line = "";
+ }
+ if (i == selectedString) {
+ if (isAbsoluteActive()) {
+ color = getTheme().getColor("tlist.selected");
+ } else {
+ color = getTheme().getColor("tlist.selected.inactive");
+ }
+ } else if (isAbsoluteActive()) {
+ color = getTheme().getColor("tlist");
+ } else {
+ color = getTheme().getColor("tlist.inactive");
+ }
+ String formatString = "%-" + Integer.toString(getWidth() - 1) + "s";
+ putStringXY(0, topY, String.format(formatString, line), color);
+ topY++;
+ if (topY >= getHeight() - 1) {
+ break;
+ }
+ }
+
+ if (isAbsoluteActive()) {
+ color = getTheme().getColor("tlist");
+ } else {
+ color = getTheme().getColor("tlist.inactive");
+ }
+
+ // Pad the rest with blank lines
+ for (int i = topY; i < getHeight() - 1; i++) {
+ hLineXY(0, i, getWidth() - 1, ' ', color);
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // TList ------------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get the selection index.
+ *
+ * @return -1 if nothing is selected, otherwise the index into the list
+ */
+ public final int getSelectedIndex() {
+ return selectedString;
+ }
+
+ /**
+ * Set the selected string index.
+ *
+ * @param index -1 to unselect, otherwise the index into the list
+ */
+ public final void setSelectedIndex(final int index) {
+ selectedString = index;
+ }
+
+ /**
+ * Get a selectable string by index.
+ *
+ * @param idx index into list
+ * @return the string at idx in the list
+ */
+ public final String getListItem(final int idx) {
+ return strings.get(idx);
+ }
+
+ /**
+ * Get the selected string.
+ *
+ * @return the selected string, or null of nothing is selected yet
+ */
+ public final String getSelected() {
+ if ((selectedString >= 0) && (selectedString <= strings.size() - 1)) {
+ return strings.get(selectedString);
+ }
+ return null;
+ }
+
+ /**
+ * Get the maximum selection index value.
+ *
+ * @return -1 if the list is empty
+ */
+ public final int getMaxSelectedIndex() {
+ return strings.size() - 1;
+ }
+
+ /**
+ * Get a copy of the list of strings to display.
+ *
+ * @return the list of strings
+ */
+ public final List<String> getList() {
+ return new ArrayList<String>(strings);
+ }
+
+ /**
+ * Set the new list of strings to display.
+ *
+ * @param list new list of strings
+ */
+ public final void setList(final List<String> list) {
+ strings.clear();
+ strings.addAll(list);
+ reflowData();
+ }
+
+ /**
+ * Perform user selection action.
+ */
+ public void dispatchEnter() {
+ assert (selectedString >= 0);
+ assert (selectedString < strings.size());
+ if (enterAction != null) {
+ enterAction.DO(this);
+ }
+ }
+
+ /**
+ * Perform list movement action.
+ */
+ public void dispatchMove() {
+ assert (selectedString >= 0);
+ assert (selectedString < strings.size());
+ if (moveAction != null) {
+ moveAction.DO(this);
+ }
+ }
+
+ /**
+ * Perform single-click action.
+ */
+ public void dispatchSingleClick() {
+ assert (selectedString >= 0);
+ assert (selectedString < strings.size());
+ if (singleClickAction != null) {
+ singleClickAction.DO(this);
+ }
+ }
+
+ }
--- /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.ArrayList;
+ import java.util.List;
+ import java.util.ResourceBundle;
+
+ import jexer.bits.StringUtils;
+ import jexer.event.TKeypressEvent;
+ import static jexer.TKeypress.*;
+
+ /**
+ * TMessageBox is a system-modal dialog with buttons for OK, Cancel, Yes, or
+ * No. Call it like:
+ *
+ * <pre>
+ * {@code
+ * box = messageBox(title, caption,
+ * TMessageBox.Type.OK | TMessageBox.Type.CANCEL);
+ *
+ * if (box.getResult() == TMessageBox.OK) {
+ * ... the user pressed OK, do stuff ...
+ * }
+ * }
+ * </pre>
+ *
+ */
+ public class TMessageBox extends TWindow {
+
+ /**
+ * Translated strings.
+ */
+ private static final ResourceBundle i18n = ResourceBundle.getBundle(TMessageBox.class.getName());
+
+ // ------------------------------------------------------------------------
+ // Constants --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Message boxes have these supported types.
+ */
+ public enum Type {
+ /**
+ * Show an OK button.
+ */
+ OK,
+
+ /**
+ * Show both OK and Cancel buttons.
+ */
+ OKCANCEL,
+
+ /**
+ * Show both Yes and No buttons.
+ */
+ YESNO,
+
+ /**
+ * Show Yes, No, and Cancel buttons.
+ */
+ YESNOCANCEL
+ };
+
+ /**
+ * Message boxes have these possible results.
+ */
+ public enum Result {
+ /**
+ * User clicked "OK".
+ */
+ OK,
+
+ /**
+ * User clicked "Cancel".
+ */
+ CANCEL,
+
+ /**
+ * User clicked "Yes".
+ */
+ YES,
+
+ /**
+ * User clicked "No".
+ */
+ NO
+ };
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The type of this message box.
+ */
+ private Type type;
+
+ /**
+ * My buttons.
+ */
+ private List<TButton> buttons;
+
+ /**
+ * Which button was clicked: OK, CANCEL, YES, or NO.
+ */
+ private Result result = Result.OK;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor. The message box will be centered on screen.
+ *
+ * @param application TApplication that manages this window
+ * @param title window title, will be centered along the top border
+ * @param caption message to display. Use embedded newlines to get a
+ * multi-line box.
+ */
+ public TMessageBox(final TApplication application, final String title,
+ final String caption) {
+
+ this(application, title, caption, Type.OK, true);
+ }
+
+ /**
+ * Public constructor. The message box will be centered on screen.
+ *
+ * @param application TApplication that manages this window
+ * @param title window title, will be centered along the top border
+ * @param caption message to display. Use embedded newlines to get a
+ * multi-line box.
+ * @param type one of the Type constants. Default is Type.OK.
+ */
+ public TMessageBox(final TApplication application, final String title,
+ final String caption, final Type type) {
+
+ this(application, title, caption, type, true);
+ }
+
+ /**
+ * Public constructor. The message box will be centered on screen.
+ *
+ * @param application TApplication that manages this window
+ * @param title window title, will be centered along the top border
+ * @param caption message to display. Use embedded newlines to get a
+ * multi-line box.
+ * @param type one of the Type constants. Default is Type.OK.
+ * @param yield if true, yield this Thread. Subclasses need to set this
+ * to false and yield at their end of their constructor intead.
+ */
+ protected TMessageBox(final TApplication application, final String title,
+ final String caption, final Type type, final boolean yield) {
+
+ // Start as 100x100 at (1, 1). These will be changed later.
+ super(application, title, 1, 1, 100, 100, CENTERED | MODAL);
+
+ // Hang onto type so that we can provide more convenience in
+ // onKeypress().
+ this.type = type;
+
+ // Determine width and height
+ String [] lines = caption.split("\n");
+ int width = StringUtils.width(title) + 12;
+ setHeight(6 + lines.length);
+ for (String line: lines) {
+ if (StringUtils.width(line) + 4 > width) {
+ width = StringUtils.width(line) + 4;
+ }
+ }
+ setWidth(width);
+ if (getWidth() > getScreen().getWidth()) {
+ setWidth(getScreen().getWidth());
+ }
+ // Re-center window to get an appropriate (x, y)
+ center();
+
+ // Now add my elements
+ int lineI = 1;
+ for (String line: lines) {
+ addLabel(line, 1, lineI, "twindow.background.modal");
+ lineI++;
+ }
+
+ // The button line
+ lineI++;
+ buttons = new ArrayList<TButton>();
+
+ int buttonX = 0;
+
+ // Setup button actions
+ switch (type) {
+
+ case OK:
+ result = Result.OK;
+ if (getWidth() < 15) {
+ setWidth(15);
+ }
+ buttonX = (getWidth() - 11) / 2;
+ buttons.add(addButton(i18n.getString("okButton"), buttonX, lineI,
+ new TAction() {
+ public void DO() {
+ result = Result.OK;
+ getApplication().closeWindow(TMessageBox.this);
+ }
+ }
+ )
+ );
+ break;
+
+ case OKCANCEL:
+ result = Result.CANCEL;
+ if (getWidth() < 26) {
+ setWidth(26);
+ }
+ buttonX = (getWidth() - 22) / 2;
+ buttons.add(addButton(i18n.getString("okButton"), buttonX, lineI,
+ new TAction() {
+ public void DO() {
+ result = Result.OK;
+ getApplication().closeWindow(TMessageBox.this);
+ }
+ }
+ )
+ );
+ buttonX += 8 + 4;
+ buttons.add(addButton(i18n.getString("cancelButton"), buttonX, lineI,
+ new TAction() {
+ public void DO() {
+ result = Result.CANCEL;
+ getApplication().closeWindow(TMessageBox.this);
+ }
+ }
+ )
+ );
+ break;
+
+ case YESNO:
+ result = Result.NO;
+ if (getWidth() < 20) {
+ setWidth(20);
+ }
+ buttonX = (getWidth() - 16) / 2;
+ buttons.add(addButton(i18n.getString("yesButton"), buttonX, lineI,
+ new TAction() {
+ public void DO() {
+ result = Result.YES;
+ getApplication().closeWindow(TMessageBox.this);
+ }
+ }
+ )
+ );
+ buttonX += 5 + 4;
+ buttons.add(addButton(i18n.getString("noButton"), buttonX, lineI,
+ new TAction() {
+ public void DO() {
+ result = Result.NO;
+ getApplication().closeWindow(TMessageBox.this);
+ }
+ }
+ )
+ );
+ break;
+
+ case YESNOCANCEL:
+ result = Result.CANCEL;
+ if (getWidth() < 31) {
+ setWidth(31);
+ }
+ buttonX = (getWidth() - 27) / 2;
+ buttons.add(addButton(i18n.getString("yesButton"), buttonX, lineI,
+ new TAction() {
+ public void DO() {
+ result = Result.YES;
+ getApplication().closeWindow(TMessageBox.this);
+ }
+ }
+ )
+ );
+ buttonX += 5 + 4;
+ buttons.add(addButton(i18n.getString("noButton"), buttonX, lineI,
+ new TAction() {
+ public void DO() {
+ result = Result.NO;
+ getApplication().closeWindow(TMessageBox.this);
+ }
+ }
+ )
+ );
+ buttonX += 4 + 4;
+ buttons.add(addButton(i18n.getString("cancelButton"), buttonX,
+ lineI,
+ new TAction() {
+ public void DO() {
+ result = Result.CANCEL;
+ getApplication().closeWindow(TMessageBox.this);
+ }
+ }
+ )
+ );
+ break;
+
+ default:
+ throw new IllegalArgumentException("Invalid message box type: " +
+ type);
+ }
+
+ if (yield) {
+ // Set the secondaryThread to run me
+ getApplication().enableSecondaryEventReceiver(this);
+
+ // Yield to the secondary thread. When I come back from the
+ // constructor response will already be set.
+ getApplication().yield();
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // TWindow ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Handle keystrokes.
+ *
+ * @param keypress keystroke event
+ */
+ @Override
+ public void onKeypress(final TKeypressEvent keypress) {
+
+ if (this instanceof TInputBox) {
+ super.onKeypress(keypress);
+ return;
+ }
+
+ // Some convenience for message boxes: Alt won't be needed for the
+ // buttons.
+ switch (type) {
+
+ case OK:
+ if (keypress.equals(kbO)) {
+ buttons.get(0).dispatch();
+ return;
+ }
+ break;
+
+ case OKCANCEL:
+ if (keypress.equals(kbO)) {
+ buttons.get(0).dispatch();
+ return;
+ } else if (keypress.equals(kbC)) {
+ buttons.get(1).dispatch();
+ return;
+ }
+ break;
+
+ case YESNO:
+ if (keypress.equals(kbY)) {
+ buttons.get(0).dispatch();
+ return;
+ } else if (keypress.equals(kbN)) {
+ buttons.get(1).dispatch();
+ return;
+ }
+ break;
+
+ case YESNOCANCEL:
+ if (keypress.equals(kbY)) {
+ buttons.get(0).dispatch();
+ return;
+ } else if (keypress.equals(kbN)) {
+ buttons.get(1).dispatch();
+ return;
+ } else if (keypress.equals(kbC)) {
+ buttons.get(2).dispatch();
+ return;
+ }
+ break;
+
+ default:
+ throw new IllegalArgumentException("Invalid message box type: " +
+ type);
+ }
+
+ super.onKeypress(keypress);
+ }
+
+ // ------------------------------------------------------------------------
+ // TMessageBox ------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get the result.
+ *
+ * @return the result: OK, CANCEL, YES, or NO.
+ */
+ public final Result getResult() {
+ return result;
+ }
+
+ /**
+ * See if the user clicked YES.
+ *
+ * @return true if the user clicked YES
+ */
+ public final boolean isYes() {
+ return (result == Result.YES);
+ }
+
+ /**
+ * See if the user clicked NO.
+ *
+ * @return true if the user clicked NO
+ */
+ public final boolean isNo() {
+ return (result == Result.NO);
+ }
+
+ /**
+ * See if the user clicked OK.
+ *
+ * @return true if the user clicked OK
+ */
+ public final boolean isOk() {
+ return (result == Result.OK);
+ }
+
+ /**
+ * See if the user clicked CANCEL.
+ *
+ * @return true if the user clicked CANCEL
+ */
+ public final boolean isCancel() {
+ return (result == Result.CANCEL);
+ }
+
+ }
--- /dev/null
+ okButton=\ \ &OK\ \
+ cancelButton=&Cancel
+ yesButton=&Yes
+ noButton=&No
--- /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 jexer.event.TResizeEvent;
+
+ /**
+ * TPanel is an empty container for other widgets.
+ */
+ public class TPanel extends TWidget {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of widget
+ * @param height height of widget
+ */
+ public TPanel(final TWidget parent, final int x, final int y,
+ final int width, final int height) {
+
+ super(parent, x, y, width, height);
+ }
+
+ // ------------------------------------------------------------------------
+ // Event handlers ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Method that subclasses can override to handle window/screen resize
+ * events.
+ *
+ * @param resize resize event
+ */
+ @Override
+ public void onResize(final TResizeEvent resize) {
+ if (resize.getType() == TResizeEvent.Type.WIDGET) {
+ if (getChildren().size() == 1) {
+ TWidget child = getChildren().get(0);
+ if ((child instanceof TSplitPane)
+ || (child instanceof TPanel)
+ ) {
+ child.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
+ resize.getWidth(), resize.getHeight()));
+ }
+ return;
+ }
+ }
+
+ // Pass on to TWidget.
+ super.onResize(resize);
+ }
+
+ // ------------------------------------------------------------------------
+ // TWidget ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+
+ // ------------------------------------------------------------------------
+ // TPanel -----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+
+ }
--- /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 jexer.bits.CellAttributes;
+ import jexer.bits.GraphicsChars;
+ import jexer.bits.StringUtils;
+
+ /**
+ * TPasswordField implements an editable text field that displays
+ * stars/asterisks when it is not active.
+ */
+ public class TPasswordField extends TField {
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width visible text width
+ * @param fixed if true, the text cannot exceed the display width
+ */
+ public TPasswordField(final TWidget parent, final int x, final int y,
+ final int width, final boolean fixed) {
+
+ this(parent, x, y, width, fixed, "", null, null);
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width visible text width
+ * @param fixed if true, the text cannot exceed the display width
+ * @param text initial text, default is empty string
+ */
+ public TPasswordField(final TWidget parent, final int x, final int y,
+ final int width, final boolean fixed, final String text) {
+
+ this(parent, x, y, width, fixed, text, null, null);
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width visible text width
+ * @param fixed if true, the text cannot exceed the display width
+ * @param text initial text, default is empty string
+ * @param enterAction function to call when enter key is pressed
+ * @param updateAction function to call when the text is updated
+ */
+ public TPasswordField(final TWidget parent, final int x, final int y,
+ final int width, final boolean fixed, final String text,
+ final TAction enterAction, final TAction updateAction) {
+
+ // Set parent and window
+ super(parent, x, y, width, fixed, text, enterAction, updateAction);
+ }
+
+ // ------------------------------------------------------------------------
+ // TField -----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Draw the text field.
+ */
+ @Override
+ public void draw() {
+ CellAttributes fieldColor;
+
+ boolean showStars = false;
+ if (isAbsoluteActive()) {
+ fieldColor = getTheme().getColor("tfield.active");
+ } else {
+ fieldColor = getTheme().getColor("tfield.inactive");
+ showStars = true;
+ }
+
+ int end = windowStart + getWidth();
+ if (end > StringUtils.width(text)) {
+ end = StringUtils.width(text);
+ }
+
+ hLineXY(0, 0, getWidth(), backgroundChar, fieldColor);
+ if (showStars) {
+ hLineXY(0, 0, getWidth() - 2, '*', fieldColor);
+ } else {
+ putStringXY(0, 0, text.substring(screenToTextPosition(windowStart),
+ screenToTextPosition(end)), fieldColor);
+ }
+
+ // Fix the cursor, it will be rendered by TApplication.drawAll().
+ updateCursor();
+ }
+
+ }
--- /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 jexer.bits.CellAttributes;
+ import jexer.bits.GraphicsChars;
+ import jexer.bits.StringUtils;
+
+ /**
+ * TProgressBar implements a simple progress bar.
+ */
+ public class TProgressBar extends TWidget {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Value that corresponds to 0% progress.
+ */
+ private int minValue = 0;
+
+ /**
+ * Value that corresponds to 100% progress.
+ */
+ private int maxValue = 100;
+
+ /**
+ * Current value of the progress.
+ */
+ private int value = 0;
+
+ /**
+ * The left border character.
+ */
+ private int leftBorderChar = GraphicsChars.CP437[0xC3];
+
+ /**
+ * The filled-in part of the bar.
+ */
+ private int completedChar = GraphicsChars.BOX;
+
+ /**
+ * The remaining to be filled in part of the bar.
+ */
+ private int remainingChar = GraphicsChars.SINGLE_BAR;
+
+ /**
+ * The right border character.
+ */
+ private int rightBorderChar = GraphicsChars.CP437[0xB4];
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of progress bar
+ * @param value initial value of percent complete
+ */
+ public TProgressBar(final TWidget parent, final int x, final int y,
+ final int width, final int value) {
+
+ // Set parent and window
+ super(parent, false, x, y, width, 1);
+
+ this.value = value;
+ }
+
+ // ------------------------------------------------------------------------
+ // Event handlers ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+
+ // ------------------------------------------------------------------------
+ // TWidget ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Override TWidget's height: we can only set height at construction
+ * time.
+ *
+ * @param height new widget height (ignored)
+ */
+ @Override
+ public void setHeight(final int height) {
+ // Do nothing
+ }
+
+ /**
+ * Draw a static progress bar.
+ */
+ @Override
+ public void draw() {
+
+ if (getWidth() <= 2) {
+ // Bail out, we are too narrow to draw anything.
+ return;
+ }
+
+ CellAttributes completeColor = getTheme().getColor("tprogressbar.complete");
+ CellAttributes incompleteColor = getTheme().getColor("tprogressbar.incomplete");
+
+ float progress = ((float)value - minValue) / ((float)maxValue - minValue);
+ int progressInt = (int)(progress * 100);
+ int progressUnit = 100 / (getWidth() - 2);
+
+ putCharXY(0, 0, leftBorderChar, incompleteColor);
+ for (int i = StringUtils.width(leftBorderChar); i < getWidth() - 2;) {
+ float iProgress = (float)i / (getWidth() - 2);
+ int iProgressInt = (int)(iProgress * 100);
+ if (iProgressInt <= progressInt - progressUnit) {
+ putCharXY(i, 0, completedChar, completeColor);
+ i += StringUtils.width(completedChar);
+ } else {
+ putCharXY(i, 0, remainingChar, incompleteColor);
+ i += StringUtils.width(remainingChar);
+ }
+ }
+ if (value >= maxValue) {
+ putCharXY(getWidth() - StringUtils.width(leftBorderChar) -
+ StringUtils.width(rightBorderChar), 0, completedChar,
+ completeColor);
+ } else {
+ putCharXY(getWidth() - StringUtils.width(leftBorderChar) -
+ StringUtils.width(rightBorderChar), 0, remainingChar,
+ incompleteColor);
+ }
+ putCharXY(getWidth() - StringUtils.width(rightBorderChar), 0,
+ rightBorderChar, incompleteColor);
+ }
+
+ // ------------------------------------------------------------------------
+ // TProgressBar -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get the value that corresponds to 0% progress.
+ *
+ * @return the value that corresponds to 0% progress
+ */
+ public int getMinValue() {
+ return minValue;
+ }
+
+ /**
+ * Set the value that corresponds to 0% progress.
+ *
+ * @param minValue the value that corresponds to 0% progress
+ */
+ public void setMinValue(final int minValue) {
+ this.minValue = minValue;
+ }
+
+ /**
+ * Get the value that corresponds to 100% progress.
+ *
+ * @return the value that corresponds to 100% progress
+ */
+ public int getMaxValue() {
+ return maxValue;
+ }
+
+ /**
+ * Set the value that corresponds to 100% progress.
+ *
+ * @param maxValue the value that corresponds to 100% progress
+ */
+ public void setMaxValue(final int maxValue) {
+ this.maxValue = maxValue;
+ }
+
+ /**
+ * Get the current value of the progress.
+ *
+ * @return the current value of the progress
+ */
+ public int getValue() {
+ return value;
+ }
+
+ /**
+ * Set the current value of the progress.
+ *
+ * @param value the current value of the progress
+ */
+ public void setValue(final int value) {
+ this.value = value;
+ }
+
+ /**
+ * Set the left border character.
+ *
+ * @param ch the char to use
+ */
+ public void setLeftBorderChar(final int ch) {
+ leftBorderChar = ch;
+ }
+
+ /**
+ * Get the left border character.
+ *
+ * @return the char
+ */
+ public int getLeftBorderChar() {
+ return leftBorderChar;
+ }
+
+ /**
+ * Set the filled-in part of the bar.
+ *
+ * @param ch the char to use
+ */
+ public void setCompletedChar(final int ch) {
+ completedChar = ch;
+ }
+
+ /**
+ * Get the filled-in part of the bar.
+ *
+ * @return the char
+ */
+ public int getCompletedChar() {
+ return completedChar;
+ }
+
+ /**
+ * Set the remaining to be filled in part of the bar.
+ *
+ * @param ch the char to use
+ */
+ public void setRemainingChar(final int ch) {
+ remainingChar = ch;
+ }
+
+ /**
+ * Get the remaining to be filled in part of the bar.
+ *
+ * @return the char
+ */
+ public int getRemainingChar() {
+ return remainingChar;
+ }
+
+ /**
+ * Set the right border character.
+ *
+ * @param ch the char to use
+ */
+ public void setRightBorderChar(final int ch) {
+ rightBorderChar = ch;
+ }
+
+ /**
+ * Get the right border character.
+ *
+ * @return the char
+ */
+ public int getRightBorderChar() {
+ return rightBorderChar;
+ }
+
+ }
--- /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 jexer.bits.CellAttributes;
+ import jexer.bits.GraphicsChars;
+ import jexer.bits.MnemonicString;
+ import jexer.bits.StringUtils;
+ import jexer.event.TKeypressEvent;
+ import jexer.event.TMouseEvent;
+ import static jexer.TKeypress.*;
+
+ /**
+ * TRadioButton implements a selectable radio button.
+ *
+ * If the user clicks or presses space on this button, it is selected.
+ *
+ * If the user presses escape on this button, it is unselected.
+ */
+ public class TRadioButton extends TWidget {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * RadioButton state, true means selected.
+ */
+ private boolean selected = false;
+
+ /**
+ * The shortcut and radio button label.
+ */
+ private MnemonicString mnemonic;
+
+ /**
+ * ID for this radio button. Buttons start counting at 1 in the
+ * RadioGroup.
+ */
+ private int id;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param x column relative to parent
+ * @param y row 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,
+ final String label, final int id) {
+
+ // Set parent and window
+ super(parent, x, y, StringUtils.width(label) + 4, 1);
+
+ mnemonic = new MnemonicString(label);
+ this.id = id;
+
+ setCursorVisible(true);
+ setCursorX(1);
+ }
+
+ // ------------------------------------------------------------------------
+ // Event handlers ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Returns true if the mouse is currently on the radio button.
+ *
+ * @param mouse mouse event
+ * @return if true the mouse is currently on the radio button
+ */
+ private boolean mouseOnRadioButton(final TMouseEvent mouse) {
+ if ((mouse.getY() == 0)
+ && (mouse.getX() >= 0)
+ && (mouse.getX() <= 2)
+ ) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Handle mouse button presses.
+ *
+ * @param mouse mouse button press event
+ */
+ @Override
+ public void onMouseDown(final TMouseEvent mouse) {
+ if ((mouseOnRadioButton(mouse)) && (mouse.isMouse1())) {
+ // Switch state
+ selected = true;
+ ((TRadioGroup) getParent()).setSelected(this);
+ }
+ }
+
+ /**
+ * Handle keystrokes.
+ *
+ * @param keypress keystroke event
+ */
+ @Override
+ public void onKeypress(final TKeypressEvent keypress) {
+
+ if (keypress.equals(kbSpace)) {
+ selected = true;
+ ((TRadioGroup) getParent()).setSelected(this);
+ return;
+ }
+
+ if (keypress.equals(kbEsc)) {
+ TRadioGroup parent = (TRadioGroup) getParent();
+ if (parent.requiresSelection == false) {
+ selected = false;
+ parent.setSelected(0);
+ }
+ return;
+ }
+
+ // Pass to parent for the things we don't care about.
+ super.onKeypress(keypress);
+ }
+
+ // ------------------------------------------------------------------------
+ // TWidget ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Override TWidget's width: we can only set width at construction time.
+ *
+ * @param width new widget width (ignored)
+ */
+ @Override
+ public void setWidth(final int width) {
+ // Do nothing
+ }
+
+ /**
+ * Override TWidget's height: we can only set height at construction
+ * time.
+ *
+ * @param height new widget height (ignored)
+ */
+ @Override
+ public void setHeight(final int height) {
+ // Do nothing
+ }
+
+ /**
+ * Draw a radio button with label.
+ */
+ @Override
+ public void draw() {
+ CellAttributes radioButtonColor;
+ CellAttributes mnemonicColor;
+
+ if (isAbsoluteActive()) {
+ radioButtonColor = getTheme().getColor("tradiobutton.active");
+ mnemonicColor = getTheme().getColor("tradiobutton.mnemonic.highlighted");
+ } else {
+ radioButtonColor = getTheme().getColor("tradiobutton.inactive");
+ mnemonicColor = getTheme().getColor("tradiobutton.mnemonic");
+ }
+
+ putCharXY(0, 0, '(', radioButtonColor);
+ if (selected) {
+ putCharXY(1, 0, GraphicsChars.CP437[0x07], radioButtonColor);
+ } else {
+ putCharXY(1, 0, ' ', radioButtonColor);
+ }
+ putCharXY(2, 0, ')', radioButtonColor);
+ putStringXY(4, 0, mnemonic.getRawLabel(), radioButtonColor);
+ if (mnemonic.getScreenShortcutIdx() >= 0) {
+ putCharXY(4 + mnemonic.getScreenShortcutIdx(), 0,
+ mnemonic.getShortcut(), mnemonicColor);
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // TRadioButton -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get RadioButton state, true means selected.
+ *
+ * @return if true then this is the one button in the group that is
+ * selected
+ */
+ public boolean isSelected() {
+ return selected;
+ }
+
+ /**
+ * Set RadioButton state, true means selected. Note package private
+ * access.
+ *
+ * @param selected if true then this is the one button in the group that
+ * is selected
+ */
+ void setSelected(final boolean selected) {
+ this.selected = selected;
+ }
+
+ /**
+ * Get ID for this radio button. Buttons start counting at 1 in the
+ * RadioGroup.
+ *
+ * @return the ID
+ */
+ public int getId() {
+ return id;
+ }
+
+ /**
+ * Get the mnemonic string for this button.
+ *
+ * @return mnemonic string
+ */
+ public MnemonicString getMnemonic() {
+ return mnemonic;
+ }
+
+ }
--- /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 jexer.bits.CellAttributes;
+ import jexer.bits.StringUtils;
+
+ /**
+ * TRadioGroup is a collection of TRadioButtons with a box and label.
+ */
+ public class TRadioGroup extends TWidget {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Label for this radio button group.
+ */
+ private String label;
+
+ /**
+ * Only one of my children can be selected.
+ */
+ private TRadioButton selectedButton = null;
+
+ /**
+ * If true, one of the children MUST be selected. Note package private
+ * access.
+ */
+ boolean requiresSelection = true;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param label label to display on the group box
+ */
+ public TRadioGroup(final TWidget parent, final int x, final int y,
+ final String label) {
+
+ // Set parent and window
+ super(parent, x, y, StringUtils.width(label) + 4, 2);
+
+ this.label = label;
+ }
+
+ // ------------------------------------------------------------------------
+ // TWidget ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Override TWidget's width: we can only set width at construction time.
+ *
+ * @param width new widget width (ignored)
+ */
+ @Override
+ public void setWidth(final int width) {
+ // Do nothing
+ }
+
+ /**
+ * Override TWidget's height: we can only set height at construction
+ * time.
+ *
+ * @param height new widget height (ignored)
+ */
+ @Override
+ public void setHeight(final int height) {
+ // Do nothing
+ }
+
+ /**
+ * Draw a radio button with label.
+ */
+ @Override
+ public void draw() {
+ CellAttributes radioGroupColor;
+
+ if (isAbsoluteActive()) {
+ radioGroupColor = getTheme().getColor("tradiogroup.active");
+ } else {
+ radioGroupColor = getTheme().getColor("tradiogroup.inactive");
+ }
+
+ drawBox(0, 0, getWidth(), getHeight(), radioGroupColor, radioGroupColor,
+ 3, false);
+
+ hLineXY(1, 0, StringUtils.width(label) + 2, ' ', radioGroupColor);
+ putStringXY(2, 0, label, radioGroupColor);
+ }
+
+ // ------------------------------------------------------------------------
+ // TRadioGroup ------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get the radio button ID that was selected.
+ *
+ * @return ID of the selected button, or 0 if no button is selected
+ */
+ public int getSelected() {
+ if (selectedButton == null) {
+ return 0;
+ }
+ 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.
+ *
+ * @param id ID of the selected button, or 0 to unselect
+ */
+ public void setSelected(final int id) {
+ if ((id < 0) || (id > getChildren().size())) {
+ return;
+ }
+
+ 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);
+ selectedButton = button;
+ }
+
+ /**
+ * Convenience function to add a radio button to this group.
+ *
+ * @param label label to display next to (right of) the radiobutton
+ * @return the new radio button
+ */
+ public TRadioButton addRadioButton(final String label) {
+ int buttonX = 1;
+ int buttonY = getChildren().size() + 1;
+ 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;
+ }
+
+ }
--- /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 jexer.event.TResizeEvent;
+
+ /**
+ * TScrollableWidget is a convenience superclass for widgets that have
+ * scrollbars.
+ */
+ public class TScrollableWidget extends TWidget implements Scrollable {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The horizontal scrollbar.
+ */
+ protected THScroller hScroller = null;
+
+ /**
+ * The vertical scrollbar.
+ */
+ protected TVScroller vScroller = null;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Protected constructor.
+ *
+ * @param parent parent widget
+ */
+ protected TScrollableWidget(final TWidget parent) {
+ super(parent);
+ }
+
+ /**
+ * Protected constructor.
+ *
+ * @param parent parent widget
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of widget
+ * @param height height of widget
+ */
+ protected TScrollableWidget(final TWidget parent, final int x, final int y,
+ final int width, final int height) {
+
+ super(parent, x, y, width, height);
+ }
+
+ /**
+ * Protected constructor used by subclasses that are disabled by default.
+ *
+ * @param parent parent widget
+ * @param enabled if true assume enabled
+ */
+ protected TScrollableWidget(final TWidget parent, final boolean enabled) {
+
+ super(parent, enabled);
+ }
+
+ /**
+ * Protected constructor used by subclasses that are disabled by default.
+ *
+ * @param parent parent widget
+ * @param enabled if true assume enabled
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of widget
+ * @param height height of widget
+ */
+ protected TScrollableWidget(final TWidget parent, final boolean enabled,
+ final int x, final int y, final int width, final int height) {
+
+ super(parent, enabled, x, y, width, height);
+ }
+
+ // ------------------------------------------------------------------------
+ // TWidget ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Handle window/screen resize events.
+ *
+ * @param event resize event
+ */
+ @Override
+ public void onResize(final TResizeEvent event) {
+ if (event.getType() == TResizeEvent.Type.WIDGET) {
+ setWidth(event.getWidth());
+ setHeight(event.getHeight());
+
+ reflowData();
+ placeScrollbars();
+ return;
+ } else {
+ super.onResize(event);
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // TScrollableWidget ------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * 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() - 1);
+ vScroller.setBigChange(getHeight() - 1);
+ }
+ }
+
+ /**
+ * Recompute whatever data is displayed by this widget.
+ */
+ public void reflowData() {
+ // Default: nothing to do
+ }
+
+ /**
+ * Get the horizontal scrollbar, or null if this Viewport does not
+ * support horizontal scrolling.
+ *
+ * @return the horizontal scrollbar
+ */
+ public THScroller getHorizontalScroller() {
+ return hScroller;
+ }
+
+ /**
+ * Get the vertical scrollbar, or null if this Viewport does not support
+ * vertical scrolling.
+ *
+ * @return the vertical scrollbar
+ */
+ public TVScroller getVerticalScroller() {
+ return vScroller;
+ }
+
+ /**
+ * Get the value that corresponds to being on the top edge of the
+ * vertical scroll bar.
+ *
+ * @return the scroll value
+ */
+ public int getTopValue() {
+ if (vScroller == null) {
+ return 0;
+ } else {
+ return vScroller.getTopValue();
+ }
+ }
+
+ /**
+ * Set the value that corresponds to being on the top edge of the
+ * vertical scroll bar.
+ *
+ * @param topValue the new scroll value
+ */
+ public void setTopValue(final int topValue) {
+ if (vScroller == null) {
+ return;
+ } else {
+ vScroller.setTopValue(topValue);
+ }
+ }
+
+ /**
+ * Get the value that corresponds to being on the bottom edge of the
+ * vertical scroll bar.
+ *
+ * @return the scroll value
+ */
+ public int getBottomValue() {
+ if (vScroller == null) {
+ return 0;
+ } else {
+ return vScroller.getBottomValue();
+ }
+ }
+
+ /**
+ * Set the value that corresponds to being on the bottom edge of the
+ * vertical scroll bar.
+ *
+ * @param bottomValue the new scroll value
+ */
+ public void setBottomValue(final int bottomValue) {
+ if (vScroller == null) {
+ return;
+ } else {
+ vScroller.setBottomValue(bottomValue);
+ }
+ }
+
+ /**
+ * Get current value of the vertical scroll.
+ *
+ * @return the scroll value
+ */
+ public int getVerticalValue() {
+ if (vScroller == null) {
+ return 0;
+ } else {
+ return vScroller.getValue();
+ }
+ }
+
+ /**
+ * Set current value of the vertical scroll.
+ *
+ * @param value the new scroll value
+ */
+ public void setVerticalValue(final int value) {
+ if (vScroller == null) {
+ return;
+ } else {
+ vScroller.setValue(value);
+ }
+ }
+
+ /**
+ * Get the increment for clicking on an arrow on the vertical scrollbar.
+ *
+ * @return the increment value
+ */
+ public int getVerticalSmallChange() {
+ if (vScroller == null) {
+ return 0;
+ } else {
+ return vScroller.getSmallChange();
+ }
+ }
+
+ /**
+ * Set the increment for clicking on an arrow on the vertical scrollbar.
+ *
+ * @param smallChange the new increment value
+ */
+ public void setVerticalSmallChange(final int smallChange) {
+ if (vScroller == null) {
+ return;
+ } else {
+ vScroller.setSmallChange(smallChange);
+ }
+ }
+
+ /**
+ * Get the increment for clicking in the bar between the box and an
+ * arrow on the vertical scrollbar.
+ *
+ * @return the increment value
+ */
+ public int getVerticalBigChange() {
+ if (vScroller == null) {
+ return 0;
+ } else {
+ return vScroller.getBigChange();
+ }
+ }
+
+ /**
+ * Set the increment for clicking in the bar between the box and an
+ * arrow on the vertical scrollbar.
+ *
+ * @param bigChange the new increment value
+ */
+ public void setVerticalBigChange(final int bigChange) {
+ if (vScroller == null) {
+ return;
+ } else {
+ vScroller.setBigChange(bigChange);
+ }
+ }
+
+ /**
+ * Perform a small step change up.
+ */
+ public void verticalDecrement() {
+ if (vScroller == null) {
+ return;
+ } else {
+ vScroller.decrement();
+ }
+ }
+
+ /**
+ * Perform a small step change down.
+ */
+ public void verticalIncrement() {
+ if (vScroller == null) {
+ return;
+ } else {
+ vScroller.increment();
+ }
+ }
+
+ /**
+ * Perform a big step change up.
+ */
+ public void bigVerticalDecrement() {
+ if (vScroller == null) {
+ return;
+ } else {
+ vScroller.bigDecrement();
+ }
+ }
+
+ /**
+ * Perform a big step change down.
+ */
+ public void bigVerticalIncrement() {
+ if (vScroller == null) {
+ return;
+ } else {
+ vScroller.bigIncrement();
+ }
+ }
+
+ /**
+ * Go to the top edge of the vertical scroller.
+ */
+ public void toTop() {
+ if (vScroller == null) {
+ return;
+ } else {
+ vScroller.toTop();
+ }
+ }
+
+ /**
+ * Go to the bottom edge of the vertical scroller.
+ */
+ public void toBottom() {
+ if (vScroller == null) {
+ return;
+ } else {
+ vScroller.toBottom();
+ }
+ }
+
+ /**
+ * Get the value that corresponds to being on the left edge of the
+ * horizontal scroll bar.
+ *
+ * @return the scroll value
+ */
+ public int getLeftValue() {
+ if (hScroller == null) {
+ return 0;
+ } else {
+ return hScroller.getLeftValue();
+ }
+ }
+
+ /**
+ * Set the value that corresponds to being on the left edge of the
+ * horizontal scroll bar.
+ *
+ * @param leftValue the new scroll value
+ */
+ public void setLeftValue(final int leftValue) {
+ if (hScroller == null) {
+ return;
+ } else {
+ hScroller.setLeftValue(leftValue);
+ }
+ }
+
+ /**
+ * Get the value that corresponds to being on the right edge of the
+ * horizontal scroll bar.
+ *
+ * @return the scroll value
+ */
+ public int getRightValue() {
+ if (hScroller == null) {
+ return 0;
+ } else {
+ return hScroller.getRightValue();
+ }
+ }
+
+ /**
+ * Set the value that corresponds to being on the right edge of the
+ * horizontal scroll bar.
+ *
+ * @param rightValue the new scroll value
+ */
+ public void setRightValue(final int rightValue) {
+ if (hScroller == null) {
+ return;
+ } else {
+ hScroller.setRightValue(rightValue);
+ }
+ }
+
+ /**
+ * Get current value of the horizontal scroll.
+ *
+ * @return the scroll value
+ */
+ public int getHorizontalValue() {
+ if (hScroller == null) {
+ return 0;
+ } else {
+ return hScroller.getValue();
+ }
+ }
+
+ /**
+ * Set current value of the horizontal scroll.
+ *
+ * @param value the new scroll value
+ */
+ public void setHorizontalValue(final int value) {
+ if (hScroller == null) {
+ return;
+ } else {
+ hScroller.setValue(value);
+ }
+ }
+
+ /**
+ * Get the increment for clicking on an arrow on the horizontal
+ * scrollbar.
+ *
+ * @return the increment value
+ */
+ public int getHorizontalSmallChange() {
+ if (hScroller == null) {
+ return 0;
+ } else {
+ return hScroller.getSmallChange();
+ }
+ }
+
+ /**
+ * Set the increment for clicking on an arrow on the horizontal
+ * scrollbar.
+ *
+ * @param smallChange the new increment value
+ */
+ public void setHorizontalSmallChange(final int smallChange) {
+ if (hScroller == null) {
+ return;
+ } else {
+ hScroller.setSmallChange(smallChange);
+ }
+ }
+
+ /**
+ * Get the increment for clicking in the bar between the box and an
+ * arrow on the horizontal scrollbar.
+ *
+ * @return the increment value
+ */
+ public int getHorizontalBigChange() {
+ if (hScroller == null) {
+ return 0;
+ } else {
+ return hScroller.getBigChange();
+ }
+ }
+
+ /**
+ * Set the increment for clicking in the bar between the box and an
+ * arrow on the horizontal scrollbar.
+ *
+ * @param bigChange the new increment value
+ */
+ public void setHorizontalBigChange(final int bigChange) {
+ if (hScroller == null) {
+ return;
+ } else {
+ hScroller.setBigChange(bigChange);
+ }
+ }
+
+ /**
+ * Perform a small step change left.
+ */
+ public void horizontalDecrement() {
+ if (hScroller == null) {
+ return;
+ } else {
+ hScroller.decrement();
+ }
+ }
+
+ /**
+ * Perform a small step change right.
+ */
+ public void horizontalIncrement() {
+ if (hScroller == null) {
+ return;
+ } else {
+ hScroller.increment();
+ }
+ }
+
+ /**
+ * Perform a big step change left.
+ */
+ public void bigHorizontalDecrement() {
+ if (hScroller == null) {
+ return;
+ } else {
+ hScroller.bigDecrement();
+ }
+ }
+
+ /**
+ * Perform a big step change right.
+ */
+ public void bigHorizontalIncrement() {
+ if (hScroller == null) {
+ return;
+ } else {
+ hScroller.bigIncrement();
+ }
+ }
+
+ /**
+ * Go to the left edge of the horizontal scroller.
+ */
+ public void toLeft() {
+ if (hScroller == null) {
+ return;
+ } else {
+ hScroller.toLeft();
+ }
+ }
+
+ /**
+ * Go to the right edge of the horizontal scroller.
+ */
+ public void toRight() {
+ if (hScroller == null) {
+ return;
+ } else {
+ hScroller.toRight();
+ }
+ }
+
+ /**
+ * Go to the top-left edge of the horizontal and vertical scrollers.
+ */
+ public void toHome() {
+ if (hScroller != null) {
+ hScroller.toLeft();
+ }
+ if (vScroller != null) {
+ vScroller.toTop();
+ }
+ }
+
+ /**
+ * Go to the bottom-right edge of the horizontal and vertical scrollers.
+ */
+ public void toEnd() {
+ if (hScroller != null) {
+ hScroller.toRight();
+ }
+ if (vScroller != null) {
+ vScroller.toBottom();
+ }
+ }
+
+ }
--- /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 jexer.event.TMouseEvent;
+ import jexer.event.TResizeEvent;
+
+ /**
+ * TScrollableWindow is a convenience superclass for windows that have
+ * scrollbars.
+ */
+ public class TScrollableWindow extends TWindow implements Scrollable {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The horizontal scrollbar.
+ */
+ protected THScroller hScroller = null;
+
+ /**
+ * The vertical scrollbar.
+ */
+ protected TVScroller vScroller = null;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor. Window will be located at (0, 0).
+ *
+ * @param application TApplication that manages this window
+ * @param title window title, will be centered along the top border
+ * @param width width of window
+ * @param height height of window
+ */
+ public TScrollableWindow(final TApplication application, final String title,
+ final int width, final int height) {
+
+ super(application, title, width, height);
+ }
+
+ /**
+ * Public constructor. Window will be located at (0, 0).
+ *
+ * @param application TApplication that manages this window
+ * @param title window title, will be centered along the top border
+ * @param width width of window
+ * @param height height of window
+ * @param flags bitmask of RESIZABLE, CENTERED, or MODAL
+ */
+ public TScrollableWindow(final TApplication application, final String title,
+ final int width, final int height, final int flags) {
+
+ super(application, title, width, height, flags);
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param application TApplication that manages this window
+ * @param title window title, will be centered along the top border
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of window
+ * @param height height of window
+ */
+ public TScrollableWindow(final TApplication application, final String title,
+ final int x, final int y, final int width, final int height) {
+
+ super(application, title, x, y, width, height);
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param application TApplication that manages this window
+ * @param title window title, will be centered along the top border
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of window
+ * @param height height of window
+ * @param flags mask of RESIZABLE, CENTERED, or MODAL
+ */
+ public TScrollableWindow(final TApplication application, final String title,
+ final int x, final int y, final int width, final int height,
+ final int flags) {
+
+ super(application, title, x, y, width, height, flags);
+ }
+
+ // ------------------------------------------------------------------------
+ // TWindow ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Handle window/screen resize events.
+ *
+ * @param event resize event
+ */
+ @Override
+ public void onResize(final TResizeEvent event) {
+ if (event.getType() == TResizeEvent.Type.WIDGET) {
+ reflowData();
+ placeScrollbars();
+ return;
+ } else {
+ super.onResize(event);
+ }
+ }
+
+ /**
+ * Maximize window.
+ */
+ @Override
+ public void maximize() {
+ super.maximize();
+ placeScrollbars();
+ }
+
+ /**
+ * Restore (unmaximize) window.
+ */
+ @Override
+ public void restore() {
+ super.restore();
+ placeScrollbars();
+ }
+
+ // ------------------------------------------------------------------------
+ // TScrollableWindow ------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * 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.setX(Math.min(Math.max(0, getWidth() - 17), 17));
+ hScroller.setY(getHeight() - 2);
+ hScroller.setWidth(getWidth() - hScroller.getX() - 3);
+ hScroller.setBigChange(getWidth() - hScroller.getX() - 3);
+ }
+ if (vScroller != null) {
+ vScroller.setX(getWidth() - 2);
+ vScroller.setHeight(getHeight() - 2);
+ vScroller.setBigChange(getHeight() - 2);
+ }
+ }
+
+ /**
+ * Recompute whatever data is displayed by this widget.
+ */
+ public void reflowData() {
+ // Default: nothing to do
+ }
+
+ /**
+ * Get the horizontal scrollbar, or null if this Viewport does not
+ * support horizontal scrolling.
+ *
+ * @return the horizontal scrollbar
+ */
+ public THScroller getHorizontalScroller() {
+ return hScroller;
+ }
+
+ /**
+ * Get the vertical scrollbar, or null if this Viewport does not support
+ * vertical scrolling.
+ *
+ * @return the vertical scrollbar
+ */
+ public TVScroller getVerticalScroller() {
+ return vScroller;
+ }
+
+ /**
+ * Get the value that corresponds to being on the top edge of the
+ * vertical scroll bar.
+ *
+ * @return the scroll value
+ */
+ public int getTopValue() {
+ if (vScroller == null) {
+ return 0;
+ } else {
+ return vScroller.getTopValue();
+ }
+ }
+
+ /**
+ * Set the value that corresponds to being on the top edge of the
+ * vertical scroll bar.
+ *
+ * @param topValue the new scroll value
+ */
+ public void setTopValue(final int topValue) {
+ if (vScroller == null) {
+ return;
+ } else {
+ vScroller.setTopValue(topValue);
+ }
+ }
+
+ /**
+ * Get the value that corresponds to being on the bottom edge of the
+ * vertical scroll bar.
+ *
+ * @return the scroll value
+ */
+ public int getBottomValue() {
+ if (vScroller == null) {
+ return 0;
+ } else {
+ return vScroller.getBottomValue();
+ }
+ }
+
+ /**
+ * Set the value that corresponds to being on the bottom edge of the
+ * vertical scroll bar.
+ *
+ * @param bottomValue the new scroll value
+ */
+ public void setBottomValue(final int bottomValue) {
+ if (vScroller == null) {
+ return;
+ } else {
+ vScroller.setBottomValue(bottomValue);
+ }
+ }
+
+ /**
+ * Get current value of the vertical scroll.
+ *
+ * @return the scroll value
+ */
+ public int getVerticalValue() {
+ if (vScroller == null) {
+ return 0;
+ } else {
+ return vScroller.getValue();
+ }
+ }
+
+ /**
+ * Set current value of the vertical scroll.
+ *
+ * @param value the new scroll value
+ */
+ public void setVerticalValue(final int value) {
+ if (vScroller == null) {
+ return;
+ } else {
+ vScroller.setValue(value);
+ }
+ }
+
+ /**
+ * Get the increment for clicking on an arrow on the vertical scrollbar.
+ *
+ * @return the increment value
+ */
+ public int getVerticalSmallChange() {
+ if (vScroller == null) {
+ return 0;
+ } else {
+ return vScroller.getSmallChange();
+ }
+ }
+
+ /**
+ * Set the increment for clicking on an arrow on the vertical scrollbar.
+ *
+ * @param smallChange the new increment value
+ */
+ public void setVerticalSmallChange(final int smallChange) {
+ if (vScroller == null) {
+ return;
+ } else {
+ vScroller.setSmallChange(smallChange);
+ }
+ }
+
+ /**
+ * Get the increment for clicking in the bar between the box and an
+ * arrow on the vertical scrollbar.
+ *
+ * @return the increment value
+ */
+ public int getVerticalBigChange() {
+ if (vScroller == null) {
+ return 0;
+ } else {
+ return vScroller.getBigChange();
+ }
+ }
+
+ /**
+ * Set the increment for clicking in the bar between the box and an
+ * arrow on the vertical scrollbar.
+ *
+ * @param bigChange the new increment value
+ */
+ public void setVerticalBigChange(final int bigChange) {
+ if (vScroller == null) {
+ return;
+ } else {
+ vScroller.setBigChange(bigChange);
+ }
+ }
+
+ /**
+ * Perform a small step change up.
+ */
+ public void verticalDecrement() {
+ if (vScroller == null) {
+ return;
+ } else {
+ vScroller.decrement();
+ }
+ }
+
+ /**
+ * Perform a small step change down.
+ */
+ public void verticalIncrement() {
+ if (vScroller == null) {
+ return;
+ } else {
+ vScroller.increment();
+ }
+ }
+
+ /**
+ * Perform a big step change up.
+ */
+ public void bigVerticalDecrement() {
+ if (vScroller == null) {
+ return;
+ } else {
+ vScroller.bigDecrement();
+ }
+ }
+
+ /**
+ * Perform a big step change down.
+ */
+ public void bigVerticalIncrement() {
+ if (vScroller == null) {
+ return;
+ } else {
+ vScroller.bigIncrement();
+ }
+ }
+
+ /**
+ * Go to the top edge of the vertical scroller.
+ */
+ public void toTop() {
+ if (vScroller == null) {
+ return;
+ } else {
+ vScroller.toTop();
+ }
+ }
+
+ /**
+ * Go to the bottom edge of the vertical scroller.
+ */
+ public void toBottom() {
+ if (vScroller == null) {
+ return;
+ } else {
+ vScroller.toBottom();
+ }
+ }
+
+ /**
+ * Get the value that corresponds to being on the left edge of the
+ * horizontal scroll bar.
+ *
+ * @return the scroll value
+ */
+ public int getLeftValue() {
+ if (hScroller == null) {
+ return 0;
+ } else {
+ return hScroller.getLeftValue();
+ }
+ }
+
+ /**
+ * Set the value that corresponds to being on the left edge of the
+ * horizontal scroll bar.
+ *
+ * @param leftValue the new scroll value
+ */
+ public void setLeftValue(final int leftValue) {
+ if (hScroller == null) {
+ return;
+ } else {
+ hScroller.setLeftValue(leftValue);
+ }
+ }
+
+ /**
+ * Get the value that corresponds to being on the right edge of the
+ * horizontal scroll bar.
+ *
+ * @return the scroll value
+ */
+ public int getRightValue() {
+ if (hScroller == null) {
+ return 0;
+ } else {
+ return hScroller.getRightValue();
+ }
+ }
+
+ /**
+ * Set the value that corresponds to being on the right edge of the
+ * horizontal scroll bar.
+ *
+ * @param rightValue the new scroll value
+ */
+ public void setRightValue(final int rightValue) {
+ if (hScroller == null) {
+ return;
+ } else {
+ hScroller.setRightValue(rightValue);
+ }
+ }
+
+ /**
+ * Get current value of the horizontal scroll.
+ *
+ * @return the scroll value
+ */
+ public int getHorizontalValue() {
+ if (hScroller == null) {
+ return 0;
+ } else {
+ return hScroller.getValue();
+ }
+ }
+
+ /**
+ * Set current value of the horizontal scroll.
+ *
+ * @param value the new scroll value
+ */
+ public void setHorizontalValue(final int value) {
+ if (hScroller == null) {
+ return;
+ } else {
+ hScroller.setValue(value);
+ }
+ }
+
+ /**
+ * Get the increment for clicking on an arrow on the horizontal
+ * scrollbar.
+ *
+ * @return the increment value
+ */
+ public int getHorizontalSmallChange() {
+ if (hScroller == null) {
+ return 0;
+ } else {
+ return hScroller.getSmallChange();
+ }
+ }
+
+ /**
+ * Set the increment for clicking on an arrow on the horizontal
+ * scrollbar.
+ *
+ * @param smallChange the new increment value
+ */
+ public void setHorizontalSmallChange(final int smallChange) {
+ if (hScroller == null) {
+ return;
+ } else {
+ hScroller.setSmallChange(smallChange);
+ }
+ }
+
+ /**
+ * Get the increment for clicking in the bar between the box and an
+ * arrow on the horizontal scrollbar.
+ *
+ * @return the increment value
+ */
+ public int getHorizontalBigChange() {
+ if (hScroller == null) {
+ return 0;
+ } else {
+ return hScroller.getBigChange();
+ }
+ }
+
+ /**
+ * Set the increment for clicking in the bar between the box and an
+ * arrow on the horizontal scrollbar.
+ *
+ * @param bigChange the new increment value
+ */
+ public void setHorizontalBigChange(final int bigChange) {
+ if (hScroller == null) {
+ return;
+ } else {
+ hScroller.setBigChange(bigChange);
+ }
+ }
+
+ /**
+ * Perform a small step change left.
+ */
+ public void horizontalDecrement() {
+ if (hScroller == null) {
+ return;
+ } else {
+ hScroller.decrement();
+ }
+ }
+
+ /**
+ * Perform a small step change right.
+ */
+ public void horizontalIncrement() {
+ if (hScroller == null) {
+ return;
+ } else {
+ hScroller.increment();
+ }
+ }
+
+ /**
+ * Perform a big step change left.
+ */
+ public void bigHorizontalDecrement() {
+ if (hScroller == null) {
+ return;
+ } else {
+ hScroller.bigDecrement();
+ }
+ }
+
+ /**
+ * Perform a big step change right.
+ */
+ public void bigHorizontalIncrement() {
+ if (hScroller == null) {
+ return;
+ } else {
+ hScroller.bigIncrement();
+ }
+ }
+
+ /**
+ * Go to the left edge of the horizontal scroller.
+ */
+ public void toLeft() {
+ if (hScroller == null) {
+ return;
+ } else {
+ hScroller.toLeft();
+ }
+ }
+
+ /**
+ * Go to the right edge of the horizontal scroller.
+ */
+ public void toRight() {
+ if (hScroller == null) {
+ return;
+ } else {
+ hScroller.toRight();
+ }
+ }
+
+ /**
+ * Go to the top-left edge of the horizontal and vertical scrollers.
+ */
+ public void toHome() {
+ if (hScroller != null) {
+ hScroller.toLeft();
+ }
+ if (vScroller != null) {
+ vScroller.toTop();
+ }
+ }
+
+ /**
+ * Go to the bottom-right edge of the horizontal and vertical scrollers.
+ */
+ public void toEnd() {
+ if (hScroller != null) {
+ hScroller.toRight();
+ }
+ if (vScroller != null) {
+ vScroller.toBottom();
+ }
+ }
+
+ /**
+ * Check if a mouse press/release/motion event coordinate is over the
+ * vertical scrollbar.
+ *
+ * @param mouse a mouse-based event
+ * @return whether or not the mouse is on the scrollbar
+ */
+ protected final boolean mouseOnVerticalScroller(final TMouseEvent mouse) {
+ if (vScroller == null) {
+ return false;
+ }
+ if ((mouse.getAbsoluteX() == vScroller.getAbsoluteX())
+ && (mouse.getAbsoluteY() >= vScroller.getAbsoluteY())
+ && (mouse.getAbsoluteY() < vScroller.getAbsoluteY() +
+ vScroller.getHeight())
+ ) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Check if a mouse press/release/motion event coordinate is over the
+ * horizontal scrollbar.
+ *
+ * @param mouse a mouse-based event
+ * @return whether or not the mouse is on the scrollbar
+ */
+ protected final boolean mouseOnHorizontalScroller(final TMouseEvent mouse) {
+ if (hScroller == null) {
+ return false;
+ }
+ if ((mouse.getAbsoluteY() == hScroller.getAbsoluteY())
+ && (mouse.getAbsoluteX() >= hScroller.getAbsoluteX())
+ && (mouse.getAbsoluteX() < hScroller.getAbsoluteX() +
+ hScroller.getWidth())
+ ) {
+ return true;
+ }
+ return false;
+ }
+
+ }
--- /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 jexer.bits.CellAttributes;
+ import jexer.bits.GraphicsChars;
+ import jexer.event.TKeypressEvent;
+ import jexer.event.TMouseEvent;
+ import static jexer.TKeypress.*;
+
+ /**
+ * TSpinner implements a simple up/down spinner.
+ */
+ public class TSpinner extends TWidget {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The action to perform when the user clicks on the up arrow.
+ */
+ private TAction upAction = null;
+
+ /**
+ * The action to perform when the user clicks on the down arrow.
+ */
+ private TAction downAction = null;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param upAction action to call when the up arrow is clicked or pressed
+ * @param downAction action to call when the down arrow is clicked or
+ * pressed
+ */
+ public TSpinner(final TWidget parent, final int x, final int y,
+ final TAction upAction, final TAction downAction) {
+
+ // Set parent and window
+ super(parent, x, y, 2, 1);
+
+ this.upAction = upAction;
+ this.downAction = downAction;
+ }
+
+ // ------------------------------------------------------------------------
+ // Event handlers ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Returns true if the mouse is currently on the up arrow.
+ *
+ * @param mouse mouse event
+ * @return true if the mouse is currently on the up arrow
+ */
+ private boolean mouseOnUpArrow(final TMouseEvent mouse) {
+ if ((mouse.getY() == 0)
+ && (mouse.getX() == getWidth() - 2)
+ ) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the mouse is currently on the down arrow.
+ *
+ * @param mouse mouse event
+ * @return true if the mouse is currently on the down arrow
+ */
+ private boolean mouseOnDownArrow(final TMouseEvent mouse) {
+ if ((mouse.getY() == 0)
+ && (mouse.getX() == getWidth() - 1)
+ ) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Handle mouse checkbox presses.
+ *
+ * @param mouse mouse button down event
+ */
+ @Override
+ public void onMouseDown(final TMouseEvent mouse) {
+ if ((mouseOnUpArrow(mouse)) && (mouse.isMouse1())) {
+ up();
+ } else if ((mouseOnDownArrow(mouse)) && (mouse.isMouse1())) {
+ down();
+ }
+ }
+
+ /**
+ * Handle keystrokes.
+ *
+ * @param keypress keystroke event
+ */
+ @Override
+ public void onKeypress(final TKeypressEvent keypress) {
+ if (keypress.equals(kbUp)) {
+ up();
+ return;
+ }
+ if (keypress.equals(kbDown)) {
+ down();
+ return;
+ }
+
+ // Pass to parent for the things we don't care about.
+ super.onKeypress(keypress);
+ }
+
+ // ------------------------------------------------------------------------
+ // TWidget ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Draw the spinner arrows.
+ */
+ @Override
+ public void draw() {
+ CellAttributes spinnerColor;
+
+ if (isAbsoluteActive()) {
+ spinnerColor = getTheme().getColor("tspinner.active");
+ } else {
+ spinnerColor = getTheme().getColor("tspinner.inactive");
+ }
+
+ putCharXY(getWidth() - 2, 0, GraphicsChars.UPARROW, spinnerColor);
+ putCharXY(getWidth() - 1, 0, GraphicsChars.DOWNARROW, spinnerColor);
+ }
+
+ // ------------------------------------------------------------------------
+ // TSpinner ---------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Perform the "up" action.
+ */
+ private void up() {
+ if (upAction != null) {
+ upAction.DO(this);
+ }
+ }
+
+ /**
+ * Perform the "down" action.
+ */
+ private void down() {
+ if (downAction != null) {
+ downAction.DO(this);
+ }
+ }
+
+ }
--- /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 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
+ * bar between them.
+ */
+ public class TSplitPane extends TWidget {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * If true, split vertically. If false, split horizontally.
+ */
+ private boolean vertical = true;
+
+ /**
+ * The location of the split bar, either as a column number for vertical
+ * split or a row number for horizontal split.
+ */
+ private int split = 0;
+
+ /**
+ * The widget on the left side.
+ */
+ private TWidget left;
+
+ /**
+ * The widget on the right side.
+ */
+ private TWidget right;
+
+ /**
+ * The widget on the top side.
+ */
+ private TWidget top;
+
+ /**
+ * The widget on the bottom side.
+ */
+ private TWidget bottom;
+
+ /**
+ * If true, we are in the middle of a split move.
+ */
+ private boolean inSplitMove = false;
+
+ /**
+ * The last seen mouse position.
+ */
+ private TMouseEvent mouse;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of widget
+ * @param height height of widget
+ * @param vertical if true, split vertically
+ */
+ public TSplitPane(final TWidget parent, final int x, final int y,
+ final int width, final int height, final boolean vertical) {
+
+ super(parent, x, y, width, height);
+
+ this.vertical = vertical;
+ center();
+ }
+
+ // ------------------------------------------------------------------------
+ // Event handlers ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Handle window/screen resize events.
+ *
+ * @param event resize event
+ */
+ @Override
+ public void onResize(final TResizeEvent event) {
+ if (event.getType() == TResizeEvent.Type.WIDGET) {
+ // Resize me
+ super.onResize(event);
+
+ if (vertical && (split >= getWidth() - 2)) {
+ center();
+ } else if (!vertical && (split >= getHeight() - 2)) {
+ center();
+ } else {
+ layoutChildren();
+ }
+ }
+ }
+
+ /**
+ * Handle mouse button presses.
+ *
+ * @param mouse mouse button event
+ */
+ @Override
+ public void onMouseDown(final TMouseEvent mouse) {
+ this.mouse = mouse;
+
+ inSplitMove = false;
+
+ if (mouse.isMouse1()) {
+ if (vertical) {
+ inSplitMove = (mouse.getAbsoluteX() - getAbsoluteX() == split);
+ } else {
+ inSplitMove = (mouse.getAbsoluteY() - getAbsoluteY() == split);
+ }
+ if (inSplitMove) {
+ return;
+ }
+ }
+
+ // I didn't take it, pass it on to my children
+ super.onMouseDown(mouse);
+ }
+
+ /**
+ * Handle mouse button releases.
+ *
+ * @param mouse mouse button release event
+ */
+ @Override
+ public void onMouseUp(final TMouseEvent mouse) {
+ this.mouse = mouse;
+
+ if (inSplitMove && mouse.isMouse1()) {
+ // Stop moving split
+ inSplitMove = false;
+ return;
+ }
+
+ // I didn't take it, pass it on to my children
+ super.onMouseUp(mouse);
+ }
+
+ /**
+ * Handle mouse movements.
+ *
+ * @param mouse mouse motion event
+ */
+ @Override
+ public void onMouseMotion(final TMouseEvent mouse) {
+ this.mouse = mouse;
+
+ if ((mouse.getAbsoluteX() - getAbsoluteX() < 0)
+ || (mouse.getAbsoluteX() - getAbsoluteX() >= getWidth())
+ || (mouse.getAbsoluteY() - getAbsoluteY() < 0)
+ || (mouse.getAbsoluteY() - getAbsoluteY() >= getHeight())
+ ) {
+ // Mouse has travelled out of my window.
+ inSplitMove = false;
+ }
+
+ if (inSplitMove) {
+ if (vertical) {
+ split = mouse.getAbsoluteX() - getAbsoluteX();
+ split = Math.min(Math.max(1, split), getWidth() - 2);
+ } else {
+ split = mouse.getAbsoluteY() - getAbsoluteY();
+ split = Math.min(Math.max(1, split), getHeight() - 2);
+ }
+ layoutChildren();
+ return;
+ }
+
+ // I didn't take it, pass it on to my children
+ super.onMouseMotion(mouse);
+ }
+
+ // ------------------------------------------------------------------------
+ // TWidget ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Draw me on screen.
+ */
+ @Override
+ public void draw() {
+ CellAttributes attr = getTheme().getColor("tsplitpane");
+ if (vertical) {
+ vLineXY(split, 0, getHeight(), GraphicsChars.WINDOW_SIDE, attr);
+ // TODO: draw intersections of children
+
+ if ((mouse != null)
+ && (mouse.getAbsoluteX() == getAbsoluteX() + split)
+ && (mouse.getAbsoluteY() >= getAbsoluteY()) &&
+ (mouse.getAbsoluteY() < getAbsoluteY() + getHeight())
+ ) {
+ putCharXY(split, mouse.getAbsoluteY() - getAbsoluteY(),
+ '\u2194', attr);
+ }
+ } else {
+ hLineXY(0, split, getWidth(), GraphicsChars.SINGLE_BAR, attr);
+ // TODO: draw intersections of children
+
+ if ((mouse != null)
+ && (mouse.getAbsoluteY() == getAbsoluteY() + split)
+ && (mouse.getAbsoluteX() >= getAbsoluteX()) &&
+ (mouse.getAbsoluteX() < getAbsoluteX() + getWidth())
+ ) {
+ putCharXY(mouse.getAbsoluteX() - getAbsoluteX(), split,
+ '\u2195', attr);
+ }
+ }
+
+ }
+
+ /**
+ * Generate a human-readable string for this widget.
+ *
+ * @return a human-readable string
+ */
+ @Override
+ public String toString() {
+ return String.format("%s(%8x) %s position (%d, %d) geometry %dx%d " +
+ "split %d left %s(%8x) right %s(%8x) top %s(%8x) bottom %s(%8x) " +
+ "active %s enabled %s visible %s", getClass().getName(),
+ hashCode(), (vertical ? "VERTICAL" : "HORIZONTAL"),
+ getX(), getY(), getWidth(), getHeight(), split,
+ (left == null ? "null" : left.getClass().getName()),
+ (left == null ? 0 : left.hashCode()),
+ (right == null ? "null" : right.getClass().getName()),
+ (right == null ? 0 : right.hashCode()),
+ (top == null ? "null" : top.getClass().getName()),
+ (top == null ? 0 : top.hashCode()),
+ (bottom == null ? "null" : bottom.getClass().getName()),
+ (bottom == null ? 0 : bottom.hashCode()),
+ isActive(), isEnabled(), isVisible());
+ }
+
+ // ------------------------------------------------------------------------
+ // TSplitPane -------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get the widget on the left side.
+ *
+ * @return the widget on the left, or null if not set
+ */
+ public TWidget getLeft() {
+ return left;
+ }
+
+ /**
+ * Set the widget on the left side.
+ *
+ * @param left the widget to set, or null to remove
+ */
+ public void setLeft(final TWidget left) {
+ if (!vertical) {
+ throw new IllegalArgumentException("cannot set left on " +
+ "horizontal split pane");
+ }
+ if (left == null) {
+ if (this.left != null) {
+ remove(this.left);
+ }
+ this.left = null;
+ return;
+ }
+ this.left = left;
+ left.setParent(this, false);
+ onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
+ getHeight()));
+ }
+
+ /**
+ * Get the widget on the right side.
+ *
+ * @return the widget on the right, or null if not set
+ */
+ public TWidget getRight() {
+ return right;
+ }
+
+ /**
+ * Set the widget on the right side.
+ *
+ * @param right the widget to set, or null to remove
+ */
+ public void setRight(final TWidget right) {
+ if (!vertical) {
+ throw new IllegalArgumentException("cannot set right on " +
+ "horizontal split pane");
+ }
+ if (right == null) {
+ if (this.right != null) {
+ remove(this.right);
+ }
+ this.right = null;
+ return;
+ }
+ this.right = right;
+ right.setParent(this, false);
+ onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
+ getHeight()));
+ }
+
+ /**
+ * Get the widget on the top side.
+ *
+ * @return the widget on the top, or null if not set
+ */
+ public TWidget getTop() {
+ return top;
+ }
+
+ /**
+ * Set the widget on the top side.
+ *
+ * @param top the widget to set, or null to remove
+ */
+ public void setTop(final TWidget top) {
+ if (vertical) {
+ throw new IllegalArgumentException("cannot set top on vertical " +
+ "split pane");
+ }
+ if (top == null) {
+ if (this.top != null) {
+ remove(this.top);
+ }
+ this.top = null;
+ return;
+ }
+ this.top = top;
+ top.setParent(this, false);
+ onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
+ getHeight()));
+ }
+
+ /**
+ * Get the widget on the bottom side.
+ *
+ * @return the widget on the bottom, or null if not set
+ */
+ public TWidget getBottom() {
+ return bottom;
+ }
+
+ /**
+ * Set the widget on the bottom side.
+ *
+ * @param bottom the widget to set, or null to remove
+ */
+ public void setBottom(final TWidget bottom) {
+ if (vertical) {
+ throw new IllegalArgumentException("cannot set bottom on " +
+ "vertical split pane");
+ }
+ if (bottom == null) {
+ if (this.bottom != null) {
+ remove(this.bottom);
+ }
+ this.bottom = null;
+ return;
+ }
+ this.bottom = bottom;
+ bottom.setParent(this, false);
+ onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
+ getHeight()));
+ }
+
+ /**
+ * Remove a widget, regardless of what pane it is on.
+ *
+ * @param widget the widget to remove
+ */
+ public void removeWidget(final TWidget widget) {
+ if (widget == null) {
+ throw new IllegalArgumentException("cannot remove null widget");
+ }
+ if (left == widget) {
+ left = null;
+ assert(right != widget);
+ assert(top != widget);
+ assert(bottom != widget);
+ return;
+ }
+ if (right == widget) {
+ right = null;
+ assert(left != widget);
+ assert(top != widget);
+ assert(bottom != widget);
+ return;
+ }
+ if (top == widget) {
+ top = null;
+ assert(left != widget);
+ assert(right != widget);
+ assert(bottom != widget);
+ return;
+ }
+ if (bottom == widget) {
+ bottom = null;
+ assert(left != widget);
+ assert(right != widget);
+ assert(top != widget);
+ return;
+ }
+ throw new IllegalArgumentException("widget " + widget +
+ " not in this split");
+ }
+
+ /**
+ * Replace a widget, regardless of what pane it is on, with another
+ * widget.
+ *
+ * @param oldWidget the widget to remove
+ * @param newWidget the widget to replace it with
+ */
+ public void replaceWidget(final TWidget oldWidget,
+ final TWidget newWidget) {
+
+ if (oldWidget == null) {
+ throw new IllegalArgumentException("cannot remove null oldWidget");
+ }
+ if (left == oldWidget) {
+ setLeft(newWidget);
+ assert(right != newWidget);
+ assert(top != newWidget);
+ assert(bottom != newWidget);
+ return;
+ }
+ if (right == oldWidget) {
+ setRight(newWidget);
+ assert(left != newWidget);
+ assert(top != newWidget);
+ assert(bottom != newWidget);
+ return;
+ }
+ if (top == oldWidget) {
+ setTop(newWidget);
+ assert(left != newWidget);
+ assert(right != newWidget);
+ assert(bottom != newWidget);
+ return;
+ }
+ if (bottom == oldWidget) {
+ setBottom(newWidget);
+ assert(left != newWidget);
+ assert(right != newWidget);
+ assert(top != newWidget);
+ return;
+ }
+ throw new IllegalArgumentException("oldWidget " + oldWidget +
+ " not in this split");
+ }
+
+ /**
+ * Layout the two child widgets.
+ */
+ private void layoutChildren() {
+ if (vertical) {
+ if (left != null) {
+ left.setDimensions(0, 0, split, getHeight());
+ left.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
+ left.getWidth(), left.getHeight()));
+ }
+ if (right != null) {
+ right.setDimensions(split + 1, 0, getWidth() - split - 1,
+ getHeight());
+ right.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
+ right.getWidth(), right.getHeight()));
+ }
+ } else {
+ if (top != null) {
+ top.setDimensions(0, 0, getWidth(), split);
+ top.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
+ top.getWidth(), top.getHeight()));
+ }
+ if (bottom != null) {
+ bottom.setDimensions(0, split + 1, getWidth(),
+ getHeight() - split - 1);
+ bottom.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
+ bottom.getWidth(), bottom.getHeight()));
+ }
+ }
+ }
+
+ /**
+ * Recenter the split to the middle of this split pane.
+ */
+ public void center() {
+ if (vertical) {
+ split = getWidth() / 2;
+ } else {
+ split = getHeight() / 2;
+ }
+ layoutChildren();
+ }
+
+ /**
+ * Remove this split, removing the widget specified.
+ *
+ * @param widgetToRemove the widget to remove
+ * @param doClose if true, call the close() method before removing the
+ * child
+ * @return the pane that remains, or null if nothing is retained
+ */
+ public TWidget removeSplit(final TWidget widgetToRemove,
+ final boolean doClose) {
+
+ TWidget keep = null;
+ if (vertical) {
+ if ((widgetToRemove != left) && (widgetToRemove != right)) {
+ throw new IllegalArgumentException("widget to remove is not " +
+ "either of the panes in this splitpane");
+ }
+ if (widgetToRemove == left) {
+ keep = right;
+ } else {
+ keep = left;
+ }
+
+ } else {
+ if ((widgetToRemove != top) && (widgetToRemove != bottom)) {
+ throw new IllegalArgumentException("widget to remove is not " +
+ "either of the panes in this splitpane");
+ }
+ if (widgetToRemove == top) {
+ keep = bottom;
+ } else {
+ keep = top;
+ }
+ }
+
+ // Remove me from my parent widget.
+ TWidget myParent = getParent();
+ remove(false);
+
+ if (keep == null) {
+ if (myParent instanceof TSplitPane) {
+ // TSplitPane has a left/right/top/bottom link to me
+ // somewhere, remove it.
+ ((TSplitPane) myParent).removeWidget(this);
+ }
+
+ // Nothing is left of either pane. Remove me and bail out.
+ return null;
+ }
+
+ if (myParent instanceof TSplitPane) {
+ // TSplitPane has a left/right/top/bottom link to me
+ // somewhere, replace me with keep.
+ ((TSplitPane) myParent).replaceWidget(this, keep);
+ } else {
+ keep.setParent(myParent, false);
+ keep.setDimensions(getX(), getY(), getWidth(), getHeight());
+ keep.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
+ getHeight()));
+ }
+
+ return keep;
+ }
+
+ }
--- /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.ArrayList;
+ import java.util.List;
+
+ import jexer.bits.CellAttributes;
+ import jexer.bits.GraphicsChars;
+ import jexer.bits.StringUtils;
+ import jexer.event.TCommandEvent;
+ import jexer.event.TKeypressEvent;
+ import jexer.event.TMouseEvent;
+
+ /**
+ * TStatusBar implements a status line with clickable buttons.
+ */
+ public class TStatusBar extends TWidget {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Remember mouse state.
+ */
+ private TMouseEvent mouse;
+
+ /**
+ * The text to display on the right side of the shortcut keys.
+ */
+ private String text = null;
+
+ /**
+ * The shortcut keys.
+ */
+ private List<TStatusBarKey> keys = new ArrayList<TStatusBarKey>();
+
+ /**
+ * A single shortcut key.
+ */
+ private class TStatusBarKey {
+
+ /**
+ * The keypress for this action.
+ */
+ public TKeypress key;
+
+ /**
+ * The command to issue.
+ */
+ public TCommand cmd;
+
+ /**
+ * The label text.
+ */
+ public String label;
+
+ /**
+ * If true, the mouse is on this key.
+ */
+ public boolean selected;
+
+ /**
+ * The left edge coordinate to draw this key with.
+ */
+ public int x = 0;
+
+ /**
+ * The width of this key on the screen.
+ *
+ * @return the number of columns this takes when drawn
+ */
+ public int width() {
+ return StringUtils.width(this.label) +
+ StringUtils.width(this.key.toString()) + 3;
+ }
+
+ /**
+ * Add a key to this status bar.
+ *
+ * @param key the key to trigger on
+ * @param cmd the command event to issue when key is pressed or this
+ * item is clicked
+ * @param label the label for this action
+ */
+ public TStatusBarKey(final TKeypress key, final TCommand cmd,
+ final String label) {
+
+ this.key = key;
+ this.cmd = cmd;
+ this.label = label;
+ }
+
+ }
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param window the window associated with this status bar
+ * @param text text for the bar on the bottom row
+ */
+ public TStatusBar(final TWindow window, final String text) {
+
+ // TStatusBar is a parentless widget, because TApplication handles
+ // its drawing and event routing directly.
+ super(null, false, 0, 0, StringUtils.width(text), 1);
+
+ this.text = text;
+ setWindow(window);
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param window the window associated with this status bar
+ */
+ public TStatusBar(final TWindow window) {
+ this(window, "");
+ }
+
+ // ------------------------------------------------------------------------
+ // Event handlers ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Handle keypresses.
+ *
+ * @param keypress keystroke event
+ * @return true if this keypress was consumed
+ */
+ public boolean statusBarKeypress(final TKeypressEvent keypress) {
+ for (TStatusBarKey key: keys) {
+ if (keypress.equals(key.key)) {
+ getApplication().postMenuEvent(new TCommandEvent(key.cmd));
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the mouse is currently on the button.
+ *
+ * @param statusBarKey the status bar item
+ * @return if true the mouse is currently on the button
+ */
+ private boolean mouseOnShortcut(final TStatusBarKey statusBarKey) {
+ if ((mouse != null)
+ && (mouse.getAbsoluteY() == getApplication().getDesktopBottom())
+ && (mouse.getAbsoluteX() >= statusBarKey.x)
+ && (mouse.getAbsoluteX() < statusBarKey.x + statusBarKey.width())
+ ) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Handle mouse button presses.
+ *
+ * @param mouse mouse button event
+ * @return true if this mouse event was consumed
+ */
+ public boolean statusBarMouseDown(final TMouseEvent mouse) {
+ this.mouse = mouse;
+
+ for (TStatusBarKey key: keys) {
+ if ((mouseOnShortcut(key)) && (mouse.isMouse1())) {
+ key.selected = true;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Handle mouse button releases.
+ *
+ * @param mouse mouse button release event
+ * @return true if this mouse event was consumed
+ */
+ public boolean statusBarMouseUp(final TMouseEvent mouse) {
+ this.mouse = mouse;
+
+ for (TStatusBarKey key: keys) {
+ if (key.selected && mouse.isMouse1()) {
+ key.selected = false;
+
+ // Dispatch the event
+ getApplication().postMenuEvent(new TCommandEvent(key.cmd));
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Handle mouse movements.
+ *
+ * @param mouse mouse motion event
+ */
+ public void statusBarMouseMotion(final TMouseEvent mouse) {
+ this.mouse = mouse;
+
+ for (TStatusBarKey key: keys) {
+ if (!mouseOnShortcut(key)) {
+ key.selected = false;
+ }
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // TWidget ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Draw the bar.
+ */
+ @Override
+ public void draw() {
+ CellAttributes barColor = new CellAttributes();
+ barColor.setTo(getTheme().getColor("tstatusbar.text"));
+ CellAttributes keyColor = new CellAttributes();
+ keyColor.setTo(getTheme().getColor("tstatusbar.button"));
+ CellAttributes selectedColor = new CellAttributes();
+ selectedColor.setTo(getTheme().getColor("tstatusbar.selected"));
+
+ // Status bar is weird. Its draw() method is called directly by
+ // TApplication after everything is drawn, and after
+ // Screen.resetClipping(). So at this point we are drawing in
+ // absolute coordinates, not relative to our TWindow.
+ int row = getScreen().getHeight() - 1;
+ int width = getScreen().getWidth();
+
+ hLineXY(0, row, width, ' ', barColor);
+
+ int col = 0;
+ for (TStatusBarKey key: keys) {
+ String keyStr = key.key.toString();
+ if (key.selected) {
+ putCharXY(col++, row, ' ', selectedColor);
+ putStringXY(col, row, keyStr, selectedColor);
+ col += StringUtils.width(keyStr);
+ putCharXY(col++, row, ' ', selectedColor);
+ putStringXY(col, row, key.label, selectedColor);
+ col += StringUtils.width(key.label);
+ putCharXY(col++, row, ' ', selectedColor);
+ } else {
+ putCharXY(col++, row, ' ', barColor);
+ putStringXY(col, row, keyStr, keyColor);
+ col += StringUtils.width(keyStr) + 1;
+ putStringXY(col, row, key.label, barColor);
+ col += StringUtils.width(key.label);
+ putCharXY(col++, row, ' ', barColor);
+ }
+ }
+ if (text.length() > 0) {
+ if (keys.size() > 0) {
+ putCharXY(col++, row, GraphicsChars.VERTICAL_BAR, barColor);
+ }
+ putCharXY(col++, row, ' ', barColor);
+ putStringXY(col, row, text, barColor);
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // TStatusBar -------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Add a key to this status bar.
+ *
+ * @param key the key to trigger on
+ * @param cmd the command event to issue when key is pressed or this item
+ * is clicked
+ * @param label the label for this action
+ */
+ public void addShortcutKeypress(final TKeypress key, final TCommand cmd,
+ final String label) {
+
+ TStatusBarKey newKey = new TStatusBarKey(key, cmd, label);
+ if (keys.size() > 0) {
+ TStatusBarKey oldKey = keys.get(keys.size() - 1);
+ newKey.x = oldKey.x + oldKey.width();
+ }
+ keys.add(newKey);
+ }
+
+ /**
+ * Set the text to display on the right side of the shortcut keys.
+ *
+ * @param text the new text
+ */
+ public void setText(final String text) {
+ this.text = text;
+ }
+
+ }
--- /dev/null
+ /*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+ package jexer;
+
+ import java.io.BufferedReader;
+ import java.io.BufferedWriter;
+ import java.io.File;
+ import java.io.FileReader;
+ import java.io.FileWriter;
+ import java.io.IOException;
+ import java.util.ArrayList;
+ import java.util.List;
+
+ import jexer.bits.CellAttributes;
+ import jexer.bits.StringUtils;
+ import jexer.event.TKeypressEvent;
+ import jexer.event.TMouseEvent;
+ import jexer.event.TResizeEvent;
+ import static jexer.TKeypress.*;
+
+ /**
+ * TTableWidget is used to display and edit regular two-dimensional tables of
+ * cells.
+ *
+ * This class was inspired by a TTable implementation originally developed by
+ * David "Niki" ROULET [niki@nikiroo.be], made available under MIT at
+ * https://github.com/nikiroo/jexer/tree/ttable_pull.
+ */
+ public class TTableWidget extends TWidget {
+
+ // ------------------------------------------------------------------------
+ // Constants --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Available borders for cells.
+ */
+ public enum Border {
+ /**
+ * No border.
+ */
+ NONE,
+
+ /**
+ * Single bar: \u2502 (vertical) and \u2500 (horizontal).
+ */
+ SINGLE,
+
+ /**
+ * Double bar: \u2551 (vertical) and \u2550 (horizontal).
+ */
+ DOUBLE,
+
+ /**
+ * Thick bar: \u2503 (vertical heavy) and \u2501 (horizontal heavy).
+ */
+ THICK,
+ }
+
+ /**
+ * If true, put a grid of numbers in the cells.
+ */
+ private static final boolean DEBUG = false;
+
+ /**
+ * Row label width.
+ */
+ private static final int ROW_LABEL_WIDTH = 8;
+
+ /**
+ * Column label height.
+ */
+ private static final int COLUMN_LABEL_HEIGHT = 1;
+
+ /**
+ * Column default width.
+ */
+ private static final int COLUMN_DEFAULT_WIDTH = 8;
+
+ /**
+ * Extra rows to add.
+ */
+ private static final int EXTRA_ROWS = (DEBUG ? 10 : 0);
+
+ /**
+ * Extra columns to add.
+ */
+ private static final int EXTRA_COLUMNS = (DEBUG ? 3 : 0);
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The underlying data, organized as columns.
+ */
+ private ArrayList<Column> columns = new ArrayList<Column>();
+
+ /**
+ * The underlying data, organized as rows.
+ */
+ private ArrayList<Row> rows = new ArrayList<Row>();
+
+ /**
+ * The row in model corresponding to the top-left visible cell.
+ */
+ private int top = 0;
+
+ /**
+ * The column in model corresponding to the top-left visible cell.
+ */
+ private int left = 0;
+
+ /**
+ * The row in model corresponding to the currently selected cell.
+ */
+ private int selectedRow = 0;
+
+ /**
+ * The column in model corresponding to the currently selected cell.
+ */
+ private int selectedColumn = 0;
+
+ /**
+ * If true, highlight the entire row of the currently-selected cell.
+ */
+ private boolean highlightRow = false;
+
+ /**
+ * If true, highlight the entire column of the currently-selected cell.
+ */
+ private boolean highlightColumn = false;
+
+ /**
+ * If true, show the row labels as the first column.
+ */
+ private boolean showRowLabels = true;
+
+ /**
+ * If true, show the column labels as the first row.
+ */
+ private boolean showColumnLabels = true;
+
+ /**
+ * The top border for the first row.
+ */
+ private Border topBorder = Border.NONE;
+
+ /**
+ * The left border for the first column.
+ */
+ private Border leftBorder = Border.NONE;
+
+ /**
+ * Column represents a column of cells.
+ */
+ public class Column {
+
+ /**
+ * X position of this column.
+ */
+ private int x = 0;
+
+ /**
+ * Width of column.
+ */
+ private int width = COLUMN_DEFAULT_WIDTH;
+
+ /**
+ * The cells of this column.
+ */
+ private ArrayList<Cell> cells = new ArrayList<Cell>();
+
+ /**
+ * Column label.
+ */
+ private String label = "";
+
+ /**
+ * The right border for this column.
+ */
+ private Border rightBorder = Border.NONE;
+
+ /**
+ * Constructor sets label to lettered column.
+ *
+ * @param col column number to use for this column. Column 0 will be
+ * "A", column 1 will be "B", column 26 will be "AA", and so on.
+ */
+ Column(int col) {
+ label = makeColumnLabel(col);
+ }
+
+ /**
+ * Add an entry to this column.
+ *
+ * @param cell the cell to add
+ */
+ public void add(final Cell cell) {
+ cells.add(cell);
+ }
+
+ /**
+ * Get an entry from this column.
+ *
+ * @param row the entry index to get
+ * @return the cell at row
+ */
+ public Cell get(final int row) {
+ return cells.get(row);
+ }
+
+ /**
+ * Get the X position of the cells in this column.
+ *
+ * @return the position
+ */
+ public int getX() {
+ return x;
+ }
+
+ /**
+ * Set the X position of the cells in this column.
+ *
+ * @param x the position
+ */
+ public void setX(final int x) {
+ for (Cell cell: cells) {
+ cell.setX(x);
+ }
+ this.x = x;
+ }
+
+ }
+
+ /**
+ * Row represents a row of cells.
+ */
+ public class Row {
+
+ /**
+ * Y position of this row.
+ */
+ private int y = 0;
+
+ /**
+ * Height of row.
+ */
+ private int height = 1;
+
+ /**
+ * The cells of this row.
+ */
+ private ArrayList<Cell> cells = new ArrayList<Cell>();
+
+ /**
+ * Row label.
+ */
+ private String label = "";
+
+ /**
+ * The bottom border for this row.
+ */
+ private Border bottomBorder = Border.NONE;
+
+ /**
+ * Constructor sets label to numbered row.
+ *
+ * @param row row number to use for this row
+ */
+ Row(final int row) {
+ label = Integer.toString(row);
+ }
+
+ /**
+ * Add an entry to this column.
+ *
+ * @param cell the cell to add
+ */
+ public void add(final Cell cell) {
+ cells.add(cell);
+ }
+
+ /**
+ * Get an entry from this row.
+ *
+ * @param column the entry index to get
+ * @return the cell at column
+ */
+ public Cell get(final int column) {
+ return cells.get(column);
+ }
+ /**
+ * Get the Y position of the cells in this column.
+ *
+ * @return the position
+ */
+ public int getY() {
+ return y;
+ }
+
+ /**
+ * Set the Y position of the cells in this column.
+ *
+ * @param y the position
+ */
+ public void setY(final int y) {
+ for (Cell cell: cells) {
+ cell.setY(y);
+ }
+ this.y = y;
+ }
+
+ }
+
+ /**
+ * Cell represents an editable cell in the table. Normally, navigation
+ * to a cell only highlights it; pressing Enter or F2 will switch to
+ * editing mode.
+ */
+ public class Cell extends TWidget {
+
+ // --------------------------------------------------------------------
+ // Variables ----------------------------------------------------------
+ // --------------------------------------------------------------------
+
+ /**
+ * The field containing the cell's data.
+ */
+ private TField field;
+
+ /**
+ * The column of this cell.
+ */
+ private int column;
+
+ /**
+ * The row of this cell.
+ */
+ private int row;
+
+ /**
+ * If true, the cell is being edited.
+ */
+ private boolean isEditing = false;
+
+ /**
+ * If true, the cell is read-only (non-editable).
+ */
+ private boolean readOnly = false;
+
+ /**
+ * Text of field before editing.
+ */
+ private String fieldText;
+
+ // --------------------------------------------------------------------
+ // Constructors -------------------------------------------------------
+ // --------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of widget
+ * @param height height of widget
+ * @param column column index of this cell
+ * @param row row index of this cell
+ */
+ public Cell(final TTableWidget parent, final int x, final int y,
+ final int width, final int height, final int column,
+ final int row) {
+
+ super(parent, x, y, width, height);
+ this.column = column;
+ this.row = row;
+
+ field = addField(0, 0, width, false);
+ field.setEnabled(false);
+ field.setBackgroundChar(' ');
+ }
+
+ // --------------------------------------------------------------------
+ // Event handlers -----------------------------------------------------
+ // --------------------------------------------------------------------
+
+ /**
+ * Handle mouse double-click events.
+ *
+ * @param mouse mouse double-click event
+ */
+ @Override
+ public void onMouseDoubleClick(final TMouseEvent mouse) {
+ // Use TWidget's code to pass the event to the children.
+ super.onMouseDown(mouse);
+
+ // Double-click means to start editing.
+ fieldText = field.getText();
+ isEditing = true;
+ field.setEnabled(true);
+ activate(field);
+
+ if (isActive()) {
+ // Let the table know that I was activated.
+ ((TTableWidget) getParent()).selectedRow = row;
+ ((TTableWidget) getParent()).selectedColumn = column;
+ ((TTableWidget) getParent()).alignGrid();
+ }
+ }
+
+ /**
+ * Handle mouse press events.
+ *
+ * @param mouse mouse button press event
+ */
+ @Override
+ public void onMouseDown(final TMouseEvent mouse) {
+ // Use TWidget's code to pass the event to the children.
+ super.onMouseDown(mouse);
+
+ if (isActive()) {
+ // Let the table know that I was activated.
+ ((TTableWidget) getParent()).selectedRow = row;
+ ((TTableWidget) getParent()).selectedColumn = column;
+ ((TTableWidget) getParent()).alignGrid();
+ }
+ }
+
+ /**
+ * Handle mouse release events.
+ *
+ * @param mouse mouse button release event
+ */
+ @Override
+ public void onMouseUp(final TMouseEvent mouse) {
+ // Use TWidget's code to pass the event to the children.
+ super.onMouseDown(mouse);
+
+ if (isActive()) {
+ // Let the table know that I was activated.
+ ((TTableWidget) getParent()).selectedRow = row;
+ ((TTableWidget) getParent()).selectedColumn = column;
+ ((TTableWidget) getParent()).alignGrid();
+ }
+ }
+
+ /**
+ * Handle keystrokes.
+ *
+ * @param keypress keystroke event
+ */
+ @Override
+ public void onKeypress(final TKeypressEvent keypress) {
+ // System.err.println("Cell onKeypress: " + keypress);
+
+ if (readOnly) {
+ // Read only: do nothing.
+ return;
+ }
+
+ if (isEditing) {
+ if (keypress.equals(kbEsc)) {
+ // ESC cancels the edit.
+ cancelEdit();
+ return;
+ }
+ if (keypress.equals(kbEnter)) {
+ // Enter ends editing.
+
+ // Pass down to field first so that it can execute
+ // enterAction if specified.
+ super.onKeypress(keypress);
+
+ fieldText = field.getText();
+ isEditing = false;
+ field.setEnabled(false);
+ return;
+ }
+ // Pass down to field.
+ super.onKeypress(keypress);
+ }
+
+ if (keypress.equals(kbEnter) || keypress.equals(kbF2)) {
+ // Enter or F2 starts editing.
+ fieldText = field.getText();
+ isEditing = true;
+ field.setEnabled(true);
+ activate(field);
+ return;
+ }
+ }
+
+ // --------------------------------------------------------------------
+ // TWidget ------------------------------------------------------------
+ // --------------------------------------------------------------------
+
+ /**
+ * Draw this cell.
+ */
+ @Override
+ public void draw() {
+ TTableWidget table = (TTableWidget) getParent();
+
+ if (isAbsoluteActive()) {
+ if (isEditing) {
+ field.setActiveColorKey("tfield.active");
+ field.setInactiveColorKey("tfield.inactive");
+ } else {
+ field.setActiveColorKey("ttable.selected");
+ field.setInactiveColorKey("ttable.selected");
+ }
+ } else if (((table.selectedColumn == column)
+ && ((table.selectedRow == row)
+ || (table.highlightColumn == true)))
+ || ((table.selectedRow == row)
+ && ((table.selectedColumn == column)
+ || (table.highlightRow == true)))
+ ) {
+ field.setActiveColorKey("ttable.active");
+ field.setInactiveColorKey("ttable.active");
+ } else {
+ field.setActiveColorKey("ttable.active");
+ field.setInactiveColorKey("ttable.inactive");
+ }
+
+ assert (isVisible() == true);
+
+ super.draw();
+ }
+
+ // --------------------------------------------------------------------
+ // TTable.Cell --------------------------------------------------------
+ // --------------------------------------------------------------------
+
+ /**
+ * Get field text.
+ *
+ * @return field text
+ */
+ public final String getText() {
+ return field.getText();
+ }
+
+ /**
+ * Set field text.
+ *
+ * @param text the new field text
+ */
+ public void setText(final String text) {
+ field.setText(text);
+ }
+
+ /**
+ * Cancel any pending edit.
+ */
+ public void cancelEdit() {
+ // Cancel any pending edit.
+ if (fieldText != null) {
+ field.setText(fieldText);
+ }
+ isEditing = false;
+ field.setEnabled(false);
+ }
+
+ /**
+ * Set an entire column of cells read-only (non-editable) or not.
+ *
+ * @param readOnly if true, the cells will be non-editable
+ */
+ public void setReadOnly(final boolean readOnly) {
+ cancelEdit();
+ this.readOnly = readOnly;
+ }
+
+ }
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of widget
+ * @param height height of widget
+ * @param gridColumns number of columns in grid
+ * @param gridRows number of rows in grid
+ */
+ public TTableWidget(final TWidget parent, final int x, final int y,
+ final int width, final int height, final int gridColumns,
+ final int gridRows) {
+
+ super(parent, x, y, width, height);
+
+ /*
+ System.err.println("gridColumns " + gridColumns +
+ " gridRows " + gridRows);
+ */
+
+ if (gridColumns < 1) {
+ throw new IllegalArgumentException("Column count cannot be less " +
+ "than 1");
+ }
+ if (gridRows < 1) {
+ throw new IllegalArgumentException("Row count cannot be less " +
+ "than 1");
+ }
+
+ // Initialize the starting row and column.
+ rows.add(new Row(0));
+ columns.add(new Column(0));
+ assert (rows.get(0).height == 1);
+
+ // Place a grid of cells that fit in this space.
+ for (int row = 0; row < gridRows; row++) {
+ for (int column = 0; column < gridColumns; column++) {
+ Cell cell = new Cell(this, 0, 0, COLUMN_DEFAULT_WIDTH, 1,
+ column, row);
+
+ if (DEBUG) {
+ // For debugging: set a grid of cell index labels.
+ cell.setText("" + row + " " + column);
+ }
+ rows.get(row).add(cell);
+ columns.get(column).add(cell);
+
+ if (columns.size() < gridColumns) {
+ columns.add(new Column(column + 1));
+ }
+ }
+ if (row < gridRows - 1) {
+ rows.add(new Row(row + 1));
+ }
+ }
+ for (int i = 0; i < rows.size(); i++) {
+ rows.get(i).setY(i + (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0));
+ }
+ for (int j = 0; j < columns.size(); j++) {
+ columns.get(j).setX((j * (COLUMN_DEFAULT_WIDTH + 1)) +
+ (showRowLabels ? ROW_LABEL_WIDTH : 0));
+ }
+ activate(columns.get(selectedColumn).get(selectedRow));
+
+ alignGrid();
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of widget
+ * @param height height of widget
+ */
+ public TTableWidget(final TWidget parent, final int x, final int y,
+ final int width, final int height) {
+
+ this(parent, x, y, width, height,
+ width / (COLUMN_DEFAULT_WIDTH + 1) + EXTRA_COLUMNS,
+ height + EXTRA_ROWS);
+ }
+
+ // ------------------------------------------------------------------------
+ // Event handlers ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Handle mouse press events.
+ *
+ * @param mouse mouse button press event
+ */
+ @Override
+ public void onMouseDown(final TMouseEvent mouse) {
+ if (mouse.isMouseWheelUp() || mouse.isMouseWheelDown()) {
+ // Treat wheel up/down as 3 up/down
+ TKeypressEvent keyEvent;
+ if (mouse.isMouseWheelUp()) {
+ keyEvent = new TKeypressEvent(kbUp);
+ } else {
+ keyEvent = new TKeypressEvent(kbDown);
+ }
+ for (int i = 0; i < 3; i++) {
+ onKeypress(keyEvent);
+ }
+ return;
+ }
+
+ // Use TWidget's code to pass the event to the children.
+ super.onMouseDown(mouse);
+ }
+
+ /**
+ * Handle keystrokes.
+ *
+ * @param keypress keystroke event
+ */
+ @Override
+ public void onKeypress(final TKeypressEvent keypress) {
+ if (keypress.equals(kbTab)
+ || keypress.equals(kbShiftTab)
+ ) {
+ // Squash tab and back-tab. They don't make sense in the TTable
+ // grid context.
+ return;
+ }
+
+ // If editing, pass to that cell and do nothing else.
+ if (getSelectedCell().isEditing) {
+ super.onKeypress(keypress);
+ return;
+ }
+
+ if (keypress.equals(kbLeft)) {
+ // Left
+ if (selectedColumn > 0) {
+ selectedColumn--;
+ }
+ activate(columns.get(selectedColumn).get(selectedRow));
+ } else if (keypress.equals(kbRight)) {
+ // Right
+ if (selectedColumn < columns.size() - 1) {
+ selectedColumn++;
+ }
+ activate(columns.get(selectedColumn).get(selectedRow));
+ } else if (keypress.equals(kbUp)) {
+ // Up
+ if (selectedRow > 0) {
+ selectedRow--;
+ }
+ activate(columns.get(selectedColumn).get(selectedRow));
+ } else if (keypress.equals(kbDown)) {
+ // Down
+ if (selectedRow < rows.size() - 1) {
+ selectedRow++;
+ }
+ activate(columns.get(selectedColumn).get(selectedRow));
+ } else if (keypress.equals(kbHome)) {
+ // Home - leftmost column
+ selectedColumn = 0;
+ activate(columns.get(selectedColumn).get(selectedRow));
+ } else if (keypress.equals(kbEnd)) {
+ // End - rightmost column
+ selectedColumn = columns.size() - 1;
+ activate(columns.get(selectedColumn).get(selectedRow));
+ } else if (keypress.equals(kbPgUp)) {
+ // PgUp - Treat like multiple up
+ for (int i = 0; i < getHeight() - 2; i++) {
+ if (selectedRow > 0) {
+ selectedRow--;
+ }
+ }
+ activate(columns.get(selectedColumn).get(selectedRow));
+ } else if (keypress.equals(kbPgDn)) {
+ // PgDn - Treat like multiple up
+ for (int i = 0; i < getHeight() - 2; i++) {
+ if (selectedRow < rows.size() - 1) {
+ selectedRow++;
+ }
+ }
+ activate(columns.get(selectedColumn).get(selectedRow));
+ } else if (keypress.equals(kbCtrlHome)) {
+ // Ctrl-Home - go to top-left
+ selectedRow = 0;
+ selectedColumn = 0;
+ activate(columns.get(selectedColumn).get(selectedRow));
+ activate(columns.get(selectedColumn).get(selectedRow));
+ } else if (keypress.equals(kbCtrlEnd)) {
+ // Ctrl-End - go to bottom-right
+ selectedRow = rows.size() - 1;
+ selectedColumn = columns.size() - 1;
+ activate(columns.get(selectedColumn).get(selectedRow));
+ activate(columns.get(selectedColumn).get(selectedRow));
+ } else {
+ // Pass to the Cell.
+ super.onKeypress(keypress);
+ }
+
+ // We may have scrolled off screen. Reset positions as needed to
+ // make the newly selected cell visible.
+ alignGrid();
+ }
+
+ /**
+ * Handle widget resize events.
+ *
+ * @param event resize event
+ */
+ @Override
+ public void onResize(final TResizeEvent event) {
+ super.onResize(event);
+
+ bottomRightCorner();
+ }
+
+ // ------------------------------------------------------------------------
+ // TWidget ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Draw the table row/column labels, and borders.
+ */
+ @Override
+ public void draw() {
+ CellAttributes labelColor = getTheme().getColor("ttable.label");
+ CellAttributes labelColorSelected = getTheme().getColor("ttable.label.selected");
+ CellAttributes borderColor = getTheme().getColor("ttable.border");
+
+ // Column labels.
+ if (showColumnLabels == true) {
+ for (int i = left; i < columns.size(); i++) {
+ if (columns.get(i).get(top).isVisible() == false) {
+ break;
+ }
+ putStringXY(columns.get(i).get(top).getX(), 0,
+ String.format(" %-" +
+ (columns.get(i).width - 2)
+ + "s ", columns.get(i).label),
+ (i == selectedColumn ? labelColorSelected : labelColor));
+ }
+ }
+
+ // Row labels.
+ if (showRowLabels == true) {
+ for (int i = top; i < rows.size(); i++) {
+ if (rows.get(i).get(left).isVisible() == false) {
+ break;
+ }
+ putStringXY(0, rows.get(i).get(left).getY(),
+ String.format(" %-6s ", rows.get(i).label),
+ (i == selectedRow ? labelColorSelected : labelColor));
+ }
+ }
+
+ // Draw vertical borders.
+ if (leftBorder == Border.SINGLE) {
+ vLineXY((showRowLabels ? ROW_LABEL_WIDTH : 0),
+ (topBorder == Border.NONE ? 0 : 1) +
+ (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0),
+ getHeight(), '\u2502', borderColor);
+ }
+ for (int i = left; i < columns.size(); i++) {
+ if (columns.get(i).get(top).isVisible() == false) {
+ break;
+ }
+ if (columns.get(i).rightBorder == Border.SINGLE) {
+ vLineXY(columns.get(i).getX() + columns.get(i).width,
+ (topBorder == Border.NONE ? 0 : 1) +
+ (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0),
+ getHeight(), '\u2502', borderColor);
+ }
+ }
+
+ // Draw horizontal borders.
+ if (topBorder == Border.SINGLE) {
+ hLineXY((showRowLabels ? ROW_LABEL_WIDTH : 0),
+ (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0),
+ getWidth(), '\u2500', borderColor);
+ }
+ for (int i = top; i < rows.size(); i++) {
+ if (rows.get(i).get(left).isVisible() == false) {
+ break;
+ }
+ if (rows.get(i).bottomBorder == Border.SINGLE) {
+ hLineXY((leftBorder == Border.NONE ? 0 : 1) +
+ (showRowLabels ? ROW_LABEL_WIDTH : 0),
+ rows.get(i).getY() + rows.get(i).height - 1,
+ getWidth(), '\u2500', borderColor);
+ } else if (rows.get(i).bottomBorder == Border.DOUBLE) {
+ hLineXY((leftBorder == Border.NONE ? 0 : 1) +
+ (showRowLabels ? ROW_LABEL_WIDTH : 0),
+ rows.get(i).getY() + rows.get(i).height - 1,
+ getWidth(), '\u2550', borderColor);
+ } else if (rows.get(i).bottomBorder == Border.THICK) {
+ hLineXY((leftBorder == Border.NONE ? 0 : 1) +
+ (showRowLabels ? ROW_LABEL_WIDTH : 0),
+ rows.get(i).getY() + rows.get(i).height - 1,
+ getWidth(), '\u2501', borderColor);
+ }
+ }
+ // Top-left corner if needed
+ if ((topBorder == Border.SINGLE) && (leftBorder == Border.SINGLE)) {
+ putCharXY((showRowLabels ? ROW_LABEL_WIDTH : 0),
+ (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0),
+ '\u250c', borderColor);
+ }
+
+ // Now draw the correct corners
+ for (int i = top; i < rows.size(); i++) {
+ if (rows.get(i).get(left).isVisible() == false) {
+ break;
+ }
+ for (int j = left; j < columns.size(); j++) {
+ if (columns.get(j).get(i).isVisible() == false) {
+ break;
+ }
+ if ((i == top) && (topBorder == Border.SINGLE)
+ && (columns.get(j).rightBorder == Border.SINGLE)
+ ) {
+ // Top tee
+ putCharXY(columns.get(j).getX() + columns.get(j).width,
+ (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0),
+ '\u252c', borderColor);
+ }
+ if ((j == left) && (leftBorder == Border.SINGLE)
+ && (rows.get(i).bottomBorder == Border.SINGLE)
+ ) {
+ // Left tee
+ putCharXY((showRowLabels ? ROW_LABEL_WIDTH : 0),
+ rows.get(i).getY() + rows.get(i).height - 1,
+ '\u251c', borderColor);
+ }
+ if ((columns.get(j).rightBorder == Border.SINGLE)
+ && (rows.get(i).bottomBorder == Border.SINGLE)
+ ) {
+ // Intersection of single bars
+ putCharXY(columns.get(j).getX() + columns.get(j).width,
+ rows.get(i).getY() + rows.get(i).height - 1,
+ '\u253c', borderColor);
+ }
+ if ((j == left) && (leftBorder == Border.SINGLE)
+ && (rows.get(i).bottomBorder == Border.DOUBLE)
+ ) {
+ // Left tee: single bar vertical, double bar horizontal
+ putCharXY((showRowLabels ? ROW_LABEL_WIDTH : 0),
+ rows.get(i).getY() + rows.get(i).height - 1,
+ '\u255e', borderColor);
+ }
+ if ((j == left) && (leftBorder == Border.SINGLE)
+ && (rows.get(i).bottomBorder == Border.THICK)
+ ) {
+ // Left tee: single bar vertical, thick bar horizontal
+ putCharXY((showRowLabels ? ROW_LABEL_WIDTH : 0),
+ rows.get(i).getY() + rows.get(i).height - 1,
+ '\u251d', borderColor);
+ }
+ if ((columns.get(j).rightBorder == Border.SINGLE)
+ && (rows.get(i).bottomBorder == Border.DOUBLE)
+ ) {
+ // Intersection: single bar vertical, double bar
+ // horizontal
+ putCharXY(columns.get(j).getX() + columns.get(j).width,
+ rows.get(i).getY() + rows.get(i).height - 1,
+ '\u256a', borderColor);
+ }
+ if ((columns.get(j).rightBorder == Border.SINGLE)
+ && (rows.get(i).bottomBorder == Border.THICK)
+ ) {
+ // Intersection: single bar vertical, thick bar
+ // horizontal
+ putCharXY(columns.get(j).getX() + columns.get(j).width,
+ rows.get(i).getY() + rows.get(i).height - 1,
+ '\u253f', borderColor);
+ }
+ }
+ }
+
+ // Now draw the window borders.
+ super.draw();
+ }
+
+ // ------------------------------------------------------------------------
+ // TTable -----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Generate the default letter name for a column number.
+ *
+ * @param col column number to use for this column. Column 0 will be
+ * "A", column 1 will be "B", column 26 will be "AA", and so on.
+ */
+ private String makeColumnLabel(int col) {
+ StringBuilder sb = new StringBuilder();
+ for (;;) {
+ sb.append((char) ('A' + (col % 26)));
+ if (col < 26) {
+ break;
+ }
+ col /= 26;
+ }
+ return sb.reverse().toString();
+ }
+
+ /**
+ * Get the currently-selected cell.
+ *
+ * @return the selected cell
+ */
+ public Cell getSelectedCell() {
+ assert (rows.get(selectedRow) != null);
+ assert (rows.get(selectedRow).get(selectedColumn) != null);
+ assert (columns.get(selectedColumn) != null);
+ assert (columns.get(selectedColumn).get(selectedRow) != null);
+ assert (rows.get(selectedRow).get(selectedColumn) ==
+ columns.get(selectedColumn).get(selectedRow));
+
+ return (columns.get(selectedColumn).get(selectedRow));
+ }
+
+ /**
+ * Get the currently-selected column.
+ *
+ * @return the selected column
+ */
+ public Column getSelectedColumn() {
+ assert (selectedColumn >= 0);
+ assert (columns.size() > selectedColumn);
+ assert (columns.get(selectedColumn) != null);
+ return columns.get(selectedColumn);
+ }
+
+ /**
+ * Get the currently-selected row.
+ *
+ * @return the selected row
+ */
+ public Row getSelectedRow() {
+ assert (selectedRow >= 0);
+ assert (rows.size() > selectedRow);
+ assert (rows.get(selectedRow) != null);
+ return rows.get(selectedRow);
+ }
+
+ /**
+ * Get the currently-selected column number. 0 is the left-most column.
+ *
+ * @return the selected column number
+ */
+ public int getSelectedColumnNumber() {
+ return selectedColumn;
+ }
+
+ /**
+ * Set the currently-selected column number. 0 is the left-most column.
+ *
+ * @param column the column number to select
+ */
+ public void setSelectedColumnNumber(final int column) {
+ if ((column < 0) || (column > columns.size() - 1)) {
+ throw new IndexOutOfBoundsException("Column count is " +
+ columns.size() + ", requested index " + column);
+ }
+ selectedColumn = column;
+ activate(columns.get(selectedColumn).get(selectedRow));
+ alignGrid();
+ }
+
+ /**
+ * Get the currently-selected row number. 0 is the top-most row.
+ *
+ * @return the selected row number
+ */
+ public int getSelectedRowNumber() {
+ return selectedRow;
+ }
+
+ /**
+ * Set the currently-selected row number. 0 is the left-most column.
+ *
+ * @param row the row number to select
+ */
+ public void setSelectedRowNumber(final int row) {
+ if ((row < 0) || (row > rows.size() - 1)) {
+ throw new IndexOutOfBoundsException("Row count is " +
+ rows.size() + ", requested index " + row);
+ }
+ selectedRow = row;
+ activate(columns.get(selectedColumn).get(selectedRow));
+ alignGrid();
+ }
+
+ /**
+ * Get the highlight row flag.
+ *
+ * @return true if the selected row is highlighted
+ */
+ public boolean getHighlightRow() {
+ return highlightRow;
+ }
+
+ /**
+ * Set the highlight row flag.
+ *
+ * @param highlightRow if true, the selected row will be highlighted
+ */
+ public void setHighlightRow(final boolean highlightRow) {
+ this.highlightRow = highlightRow;
+ }
+
+ /**
+ * Get the highlight column flag.
+ *
+ * @return true if the selected column is highlighted
+ */
+ public boolean getHighlightColumn() {
+ return highlightColumn;
+ }
+
+ /**
+ * Set the highlight column flag.
+ *
+ * @param highlightColumn if true, the selected column will be highlighted
+ */
+ public void setHighlightColumn(final boolean highlightColumn) {
+ this.highlightColumn = highlightColumn;
+ }
+
+ /**
+ * Get the show row labels flag.
+ *
+ * @return true if row labels are shown
+ */
+ public boolean getShowRowLabels() {
+ return showRowLabels;
+ }
+
+ /**
+ * Set the show row labels flag.
+ *
+ * @param showRowLabels if true, the row labels will be shown
+ */
+ public void setShowRowLabels(final boolean showRowLabels) {
+ this.showRowLabels = showRowLabels;
+ }
+
+ /**
+ * Get the show column labels flag.
+ *
+ * @return true if column labels are shown
+ */
+ public boolean getShowColumnLabels() {
+ return showColumnLabels;
+ }
+
+ /**
+ * Set the show column labels flag.
+ *
+ * @param showColumnLabels if true, the column labels will be shown
+ */
+ public void setShowColumnLabels(final boolean showColumnLabels) {
+ this.showColumnLabels = showColumnLabels;
+ }
+
+ /**
+ * Get the number of columns.
+ *
+ * @return the number of columns
+ */
+ public int getColumnCount() {
+ return columns.size();
+ }
+
+ /**
+ * Get the number of rows.
+ *
+ * @return the number of rows
+ */
+ public int getRowCount() {
+ return rows.size();
+ }
+
+
+ /**
+ * Push top and left to the bottom-most right corner of the available
+ * grid.
+ */
+ private void bottomRightCorner() {
+ int viewColumns = getWidth();
+ if (showRowLabels == true) {
+ viewColumns -= ROW_LABEL_WIDTH;
+ }
+
+ // Set left and top such that the table stays on screen if possible.
+ top = rows.size() - getHeight();
+ left = columns.size() - (getWidth() / (viewColumns / (COLUMN_DEFAULT_WIDTH + 1)));
+ // Now ensure the selection is visible.
+ alignGrid();
+ }
+
+ /**
+ * Align the grid so that the selected cell is fully visible.
+ */
+ private void alignGrid() {
+
+ /*
+ System.err.println("alignGrid() # columns " + columns.size() +
+ " # rows " + rows.size());
+ */
+
+ int viewColumns = getWidth();
+ if (showRowLabels == true) {
+ viewColumns -= ROW_LABEL_WIDTH;
+ }
+ if (leftBorder != Border.NONE) {
+ viewColumns--;
+ }
+ int viewRows = getHeight();
+ if (showColumnLabels == true) {
+ viewRows -= COLUMN_LABEL_HEIGHT;
+ }
+ if (topBorder != Border.NONE) {
+ viewRows--;
+ }
+
+ // If we pushed left or right, adjust the box to include the new
+ // selected cell.
+ if (selectedColumn < left) {
+ left = selectedColumn - 1;
+ }
+ if (left < 0) {
+ left = 0;
+ }
+ if (selectedRow < top) {
+ top = selectedRow - 1;
+ }
+ if (top < 0) {
+ top = 0;
+ }
+
+ /*
+ * viewColumns and viewRows now contain the available columns and
+ * rows available to view the selected cell. We adjust left and top
+ * to ensure the selected cell is within view, and then make all
+ * cells outside the box between (left, top) and (right, bottom)
+ * invisible.
+ *
+ * We need to calculate right and bottom now.
+ */
+ int right = left;
+
+ boolean done = false;
+ while (!done) {
+ int rightCellX = (showRowLabels ? ROW_LABEL_WIDTH : 0);
+ if (leftBorder != Border.NONE) {
+ rightCellX++;
+ }
+ int maxCellX = rightCellX + viewColumns;
+ right = left;
+ boolean selectedIsVisible = false;
+ int selectedX = 0;
+ for (int x = left; x < columns.size(); x++) {
+ if (x == selectedColumn) {
+ selectedX = rightCellX;
+ if (selectedX + columns.get(x).width + 1 <= maxCellX) {
+ selectedIsVisible = true;
+ }
+ }
+ rightCellX += columns.get(x).width + 1;
+ if (rightCellX >= maxCellX) {
+ break;
+ }
+ right++;
+ }
+ if (right < selectedColumn) {
+ // selectedColumn is outside the view range. Push left over,
+ // and calculate again.
+ left++;
+ } else if (left == selectedColumn) {
+ // selectedColumn doesn't fit inside the view range, but we
+ // can't go over any further either. Bail out.
+ done = true;
+ } else if (selectedIsVisible == false) {
+ // selectedColumn doesn't fit inside the view range, continue
+ // on.
+ left++;
+ } else {
+ // selectedColumn is fully visible, all done.
+ assert (selectedIsVisible == true);
+ done = true;
+ }
+
+ } // while (!done)
+
+ // We have the left/right range correct, set cell visibility and
+ // column X positions.
+ int leftCellX = showRowLabels ? ROW_LABEL_WIDTH : 0;
+ if (leftBorder != Border.NONE) {
+ leftCellX++;
+ }
+ for (int x = 0; x < columns.size(); x++) {
+ if ((x < left) || (x > right)) {
+ for (int i = 0; i < rows.size(); i++) {
+ columns.get(x).get(i).setVisible(false);
+ columns.get(x).setX(getWidth() + 1);
+ }
+ continue;
+ }
+ for (int i = 0; i < rows.size(); i++) {
+ columns.get(x).get(i).setVisible(true);
+ }
+ columns.get(x).setX(leftCellX);
+ leftCellX += columns.get(x).width + 1;
+ }
+
+ int bottom = top;
+
+ done = false;
+ while (!done) {
+ int bottomCellY = (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0);
+ if (topBorder != Border.NONE) {
+ bottomCellY++;
+ }
+ int maxCellY = bottomCellY + viewRows;
+ bottom = top;
+ for (int y = top; y < rows.size(); y++) {
+ bottomCellY += rows.get(y).height;
+ if (bottomCellY >= maxCellY) {
+ break;
+ }
+ bottom++;
+ }
+ if (bottom < selectedRow) {
+ // selectedRow is outside the view range. Push top down, and
+ // calculate again.
+ top++;
+ } else {
+ // selectedRow is inside the view range, done.
+ done = true;
+ }
+ } // while (!done)
+
+ // We have the top/bottom range correct, set cell visibility and
+ // row Y positions.
+ int topCellY = showColumnLabels ? COLUMN_LABEL_HEIGHT : 0;
+ if (topBorder != Border.NONE) {
+ topCellY++;
+ }
+ for (int y = 0; y < rows.size(); y++) {
+ if ((y < top) || (y > bottom)) {
+ for (int i = 0; i < columns.size(); i++) {
+ rows.get(y).get(i).setVisible(false);
+ }
+ rows.get(y).setY(getHeight() + 1);
+ continue;
+ }
+ for (int i = 0; i < columns.size(); i++) {
+ rows.get(y).get(i).setVisible(true);
+ }
+ rows.get(y).setY(topCellY);
+ topCellY += rows.get(y).height;
+ }
+
+ // Last thing: cancel any edits that are not the selected cell.
+ for (int y = 0; y < rows.size(); y++) {
+ for (int x = 0; x < columns.size(); x++) {
+ if ((x == selectedColumn) && (y == selectedRow)) {
+ continue;
+ }
+ rows.get(y).get(x).cancelEdit();
+ }
+ }
+ }
+
+ /**
+ * Load contents from file in CSV format.
+ *
+ * @param csvFile a File referencing the CSV data
+ * @throws IOException if a java.io operation throws
+ */
+ public void loadCsvFile(final File csvFile) throws IOException {
+ BufferedReader reader = null;
+
+ try {
+ reader = new BufferedReader(new FileReader(csvFile));
+
+ String line = null;
+ boolean first = true;
+ for (line = reader.readLine(); line != null;
+ line = reader.readLine()) {
+
+ List<String> list = StringUtils.fromCsv(line);
+ if (list.size() == 0) {
+ continue;
+ }
+
+ if (list.size() > columns.size()) {
+ int n = list.size() - columns.size();
+ for (int i = 0; i < n; i++) {
+ selectedColumn = columns.size() - 1;
+ insertColumnRight(selectedColumn);
+ }
+ }
+ assert (list.size() == columns.size());
+
+ if (first) {
+ // First row: just replace what is here.
+ selectedRow = 0;
+ first = false;
+ } else {
+ // All other rows: append to the end.
+ selectedRow = rows.size() - 1;
+ insertRowBelow(selectedRow);
+ selectedRow = rows.size() - 1;
+ }
+ 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) {
+ reader.close();
+ }
+ }
+
+ left = 0;
+ top = 0;
+ selectedRow = 0;
+ selectedColumn = 0;
+ alignGrid();
+ activate(columns.get(selectedColumn).get(selectedRow));
+ }
+
+ /**
+ * Save contents to file in CSV format.
+ *
+ * @param filename file to save to
+ * @throws IOException if a java.io operation throws
+ */
+ public void saveToCsvFilename(final String filename) throws IOException {
+ BufferedWriter writer = null;
+
+ try {
+ writer = new BufferedWriter(new FileWriter(filename));
+ for (Row row: rows) {
+ List<String> list = new ArrayList<String>(row.cells.size());
+ for (Cell cell: row.cells) {
+ list.add(cell.getText());
+ }
+ writer.write(StringUtils.toCsv(list));
+ writer.write("\n");
+ }
+ } finally {
+ if (writer != null) {
+ writer.close();
+ }
+ }
+ }
+
+ /**
+ * Save contents to file in text format with lines.
+ *
+ * @param filename file to save to
+ * @throws IOException if a java.io operation throws
+ */
+ public void saveToTextFilename(final String filename) throws IOException {
+ BufferedWriter writer = null;
+
+ try {
+ writer = new BufferedWriter(new FileWriter(filename));
+
+ if ((topBorder == Border.SINGLE) && (leftBorder == Border.SINGLE)) {
+ // Emit top-left corner.
+ writer.write("\u250c");
+ }
+
+ if (topBorder == Border.SINGLE) {
+ int cellI = 0;
+ for (Cell cell: rows.get(0).cells) {
+ for (int i = 0; i < columns.get(cellI).width; i++) {
+ writer.write("\u2500");
+ }
+
+ if (columns.get(cellI).rightBorder == Border.SINGLE) {
+ if (cellI < columns.size() - 1) {
+ // Emit top tee.
+ writer.write("\u252c");
+ } else {
+ // Emit top-right corner.
+ writer.write("\u2510");
+ }
+ }
+ cellI++;
+ }
+ }
+ writer.write("\n");
+
+ int rowI = 0;
+ for (Row row: rows) {
+
+ if (leftBorder == Border.SINGLE) {
+ // Emit left border.
+ writer.write("\u2502");
+ }
+
+ int cellI = 0;
+ for (Cell cell: row.cells) {
+ writer.write(String.format("%" +
+ columns.get(cellI).width + "s", cell.getText()));
+
+ if (columns.get(cellI).rightBorder == Border.SINGLE) {
+ // Emit right border.
+ writer.write("\u2502");
+ }
+ cellI++;
+ }
+ writer.write("\n");
+
+ if (row.bottomBorder == Border.NONE) {
+ // All done, move on to the next row.
+ continue;
+ }
+
+ // Emit the bottom borders and intersections.
+ if ((leftBorder == Border.SINGLE)
+ && (row.bottomBorder != Border.NONE)
+ ) {
+ if (rowI < rows.size() - 1) {
+ if (row.bottomBorder == Border.SINGLE) {
+ // Emit left tee.
+ writer.write("\u251c");
+ } else if (row.bottomBorder == Border.DOUBLE) {
+ // Emit left tee (double).
+ writer.write("\u255e");
+ } else if (row.bottomBorder == Border.THICK) {
+ // Emit left tee (thick).
+ writer.write("\u251d");
+ }
+ }
+
+ if (rowI == rows.size() - 1) {
+ if (row.bottomBorder == Border.SINGLE) {
+ // Emit left bottom corner.
+ writer.write("\u2514");
+ } else if (row.bottomBorder == Border.DOUBLE) {
+ // Emit left bottom corner (double).
+ writer.write("\u2558");
+ } else if (row.bottomBorder == Border.THICK) {
+ // Emit left bottom corner (thick).
+ writer.write("\u2515");
+ }
+ }
+ }
+
+ cellI = 0;
+ for (Cell cell: row.cells) {
+
+ for (int i = 0; i < columns.get(cellI).width; i++) {
+ if (row.bottomBorder == Border.SINGLE) {
+ writer.write("\u2500");
+ }
+ if (row.bottomBorder == Border.DOUBLE) {
+ writer.write("\u2550");
+ }
+ if (row.bottomBorder == Border.THICK) {
+ writer.write("\u2501");
+ }
+ }
+
+ if ((rowI < rows.size() - 1)
+ && (cellI == columns.size() - 1)
+ && (row.bottomBorder == Border.SINGLE)
+ && (columns.get(cellI).rightBorder == Border.SINGLE)
+ ) {
+ // Emit right tee.
+ writer.write("\u2524");
+ }
+ if ((rowI < rows.size() - 1)
+ && (cellI == columns.size() - 1)
+ && (row.bottomBorder == Border.DOUBLE)
+ && (columns.get(cellI).rightBorder == Border.SINGLE)
+ ) {
+ // Emit right tee (double).
+ writer.write("\u2561");
+ }
+ if ((rowI < rows.size() - 1)
+ && (cellI == columns.size() - 1)
+ && (row.bottomBorder == Border.THICK)
+ && (columns.get(cellI).rightBorder == Border.SINGLE)
+ ) {
+ // Emit right tee (thick).
+ writer.write("\u2525");
+ }
+ if ((rowI == rows.size() - 1)
+ && (cellI == columns.size() - 1)
+ && (row.bottomBorder == Border.SINGLE)
+ && (columns.get(cellI).rightBorder == Border.SINGLE)
+ ) {
+ // Emit right bottom corner.
+ writer.write("\u2518");
+ }
+ if ((rowI == rows.size() - 1)
+ && (cellI == columns.size() - 1)
+ && (row.bottomBorder == Border.DOUBLE)
+ && (columns.get(cellI).rightBorder == Border.SINGLE)
+ ) {
+ // Emit right bottom corner (double).
+ writer.write("\u255b");
+ }
+ if ((rowI == rows.size() - 1)
+ && (cellI == columns.size() - 1)
+ && (row.bottomBorder == Border.THICK)
+ && (columns.get(cellI).rightBorder == Border.SINGLE)
+ ) {
+ // Emit right bottom corner (thick).
+ writer.write("\u2519");
+ }
+ if ((rowI < rows.size() - 1)
+ && (cellI < columns.size() - 1)
+ && (row.bottomBorder == Border.SINGLE)
+ && (columns.get(cellI).rightBorder == Border.SINGLE)
+ ) {
+ // Emit intersection.
+ writer.write("\u253c");
+ }
+ if ((rowI < rows.size() - 1)
+ && (cellI < columns.size() - 1)
+ && (row.bottomBorder == Border.DOUBLE)
+ && (columns.get(cellI).rightBorder == Border.SINGLE)
+ ) {
+ // Emit intersection (double).
+ writer.write("\u256a");
+ }
+ if ((rowI < rows.size() - 1)
+ && (cellI < columns.size() - 1)
+ && (row.bottomBorder == Border.THICK)
+ && (columns.get(cellI).rightBorder == Border.SINGLE)
+ ) {
+ // Emit intersection (thick).
+ writer.write("\u253f");
+ }
+ if ((rowI == rows.size() - 1)
+ && (cellI < columns.size() - 1)
+ && (row.bottomBorder == Border.SINGLE)
+ && (columns.get(cellI).rightBorder == Border.SINGLE)
+ ) {
+ // Emit bottom tee.
+ writer.write("\u2534");
+ }
+ if ((rowI == rows.size() - 1)
+ && (cellI < columns.size() - 1)
+ && (row.bottomBorder == Border.DOUBLE)
+ && (columns.get(cellI).rightBorder == Border.SINGLE)
+ ) {
+ // Emit bottom tee (double).
+ writer.write("\u2567");
+ }
+ if ((rowI == rows.size() - 1)
+ && (cellI < columns.size() - 1)
+ && (row.bottomBorder == Border.THICK)
+ && (columns.get(cellI).rightBorder == Border.SINGLE)
+ ) {
+ // Emit bottom tee (thick).
+ writer.write("\u2537");
+ }
+
+ cellI++;
+ }
+
+ writer.write("\n");
+ rowI++;
+ }
+ } finally {
+ if (writer != null) {
+ writer.close();
+ }
+ }
+ }
+
+ /**
+ * Set the selected cell location.
+ *
+ * @param column the selected cell location column
+ * @param row the selected cell location row
+ */
+ public void setSelectedCell(final int column, final int row) {
+ if ((column < 0) || (column > columns.size() - 1)) {
+ throw new IndexOutOfBoundsException("Column count is " +
+ columns.size() + ", requested index " + column);
+ }
+ if ((row < 0) || (row > rows.size() - 1)) {
+ throw new IndexOutOfBoundsException("Row count is " +
+ rows.size() + ", requested index " + row);
+ }
+ selectedColumn = column;
+ selectedRow = row;
+ alignGrid();
+ }
+
+ /**
+ * Get a particular cell.
+ *
+ * @param column the cell column
+ * @param row the cell row
+ * @return the cell
+ */
+ public Cell getCell(final int column, final int row) {
+ if ((column < 0) || (column > columns.size() - 1)) {
+ throw new IndexOutOfBoundsException("Column count is " +
+ columns.size() + ", requested index " + column);
+ }
+ if ((row < 0) || (row > rows.size() - 1)) {
+ throw new IndexOutOfBoundsException("Row count is " +
+ rows.size() + ", requested index " + row);
+ }
+ return rows.get(row).get(column);
+ }
+
+ /**
+ * Get the text of a particular cell.
+ *
+ * @param column the cell column
+ * @param row the cell row
+ * @return the text in the cell
+ */
+ public String getCellText(final int column, final int row) {
+ if ((column < 0) || (column > columns.size() - 1)) {
+ throw new IndexOutOfBoundsException("Column count is " +
+ columns.size() + ", requested index " + column);
+ }
+ if ((row < 0) || (row > rows.size() - 1)) {
+ throw new IndexOutOfBoundsException("Row count is " +
+ rows.size() + ", requested index " + row);
+ }
+ return rows.get(row).get(column).getText();
+ }
+
+ /**
+ * Set the text of a particular cell.
+ *
+ * @param column the cell column
+ * @param row the cell row
+ * @param text the text to put into the cell
+ */
+ public void setCellText(final int column, final int row,
+ final String text) {
+
+ if ((column < 0) || (column > columns.size() - 1)) {
+ throw new IndexOutOfBoundsException("Column count is " +
+ columns.size() + ", requested index " + column);
+ }
+ if ((row < 0) || (row > rows.size() - 1)) {
+ throw new IndexOutOfBoundsException("Row count is " +
+ rows.size() + ", requested index " + row);
+ }
+ rows.get(row).get(column).setText(text);
+ }
+
+ /**
+ * Set the action to perform when the user presses enter on a particular
+ * cell.
+ *
+ * @param column the cell column
+ * @param row the cell row
+ * @param action the action to perform when the user presses enter on the
+ * cell
+ */
+ public void setCellEnterAction(final int column, final int row,
+ final TAction action) {
+
+ if ((column < 0) || (column > columns.size() - 1)) {
+ throw new IndexOutOfBoundsException("Column count is " +
+ columns.size() + ", requested index " + column);
+ }
+ if ((row < 0) || (row > rows.size() - 1)) {
+ throw new IndexOutOfBoundsException("Row count is " +
+ rows.size() + ", requested index " + row);
+ }
+ rows.get(row).get(column).field.setEnterAction(action);
+ }
+
+ /**
+ * Set the action to perform when the user updates a particular cell.
+ *
+ * @param column the cell column
+ * @param row the cell row
+ * @param action the action to perform when the user updates the cell
+ */
+ public void setCellUpdateAction(final int column, final int row,
+ final TAction action) {
+
+ if ((column < 0) || (column > columns.size() - 1)) {
+ throw new IndexOutOfBoundsException("Column count is " +
+ columns.size() + ", requested index " + column);
+ }
+ if ((row < 0) || (row > rows.size() - 1)) {
+ throw new IndexOutOfBoundsException("Row count is " +
+ rows.size() + ", requested index " + row);
+ }
+ rows.get(row).get(column).field.setUpdateAction(action);
+ }
+
+ /**
+ * Get the width of a column.
+ *
+ * @param column the column number
+ * @return the width of the column
+ */
+ public int getColumnWidth(final int column) {
+ if ((column < 0) || (column > columns.size() - 1)) {
+ throw new IndexOutOfBoundsException("Column count is " +
+ columns.size() + ", requested index " + column);
+ }
+ return columns.get(column).width;
+ }
+
+ /**
+ * Set the width of a column.
+ *
+ * @param column the column number
+ * @param width the new width of the column
+ */
+ public void setColumnWidth(final int column, final int width) {
+ if ((column < 0) || (column > columns.size() - 1)) {
+ throw new IndexOutOfBoundsException("Column count is " +
+ columns.size() + ", requested index " + column);
+ }
+
+ if (width < 4) {
+ // Columns may not be smaller than 4 cells wide.
+ return;
+ }
+
+ int delta = width - columns.get(column).width;
+ columns.get(column).width = width;
+ for (Cell cell: columns.get(column).cells) {
+ cell.setWidth(columns.get(column).width);
+ cell.field.setWidth(columns.get(column).width);
+ }
+ for (int i = column + 1; i < columns.size(); i++) {
+ columns.get(i).setX(columns.get(i).getX() + delta);
+ }
+ if (column == columns.size() - 1) {
+ bottomRightCorner();
+ } else {
+ alignGrid();
+ }
+ }
+
+ /**
+ * Get the label of a column.
+ *
+ * @param column the column number
+ * @return the label of the column
+ */
+ public String getColumnLabel(final int column) {
+ if ((column < 0) || (column > columns.size() - 1)) {
+ throw new IndexOutOfBoundsException("Column count is " +
+ columns.size() + ", requested index " + column);
+ }
+ return columns.get(column).label;
+ }
+
+ /**
+ * Set the label of a column.
+ *
+ * @param column the column number
+ * @param label the new label of the column
+ */
+ public void setColumnLabel(final int column, final String label) {
+ if ((column < 0) || (column > columns.size() - 1)) {
+ throw new IndexOutOfBoundsException("Column count is " +
+ columns.size() + ", requested index " + column);
+ }
+ columns.get(column).label = label;
+ }
+
+ /**
+ * Get the label of a row.
+ *
+ * @param row the row number
+ * @return the label of the row
+ */
+ public String getRowLabel(final int row) {
+ if ((row < 0) || (row > rows.size() - 1)) {
+ throw new IndexOutOfBoundsException("Row count is " +
+ rows.size() + ", requested index " + row);
+ }
+ return rows.get(row).label;
+ }
+
+ /**
+ * Set the label of a row.
+ *
+ * @param row the row number
+ * @param label the new label of the row
+ */
+ public void setRowLabel(final int row, final String label) {
+ if ((row < 0) || (row > rows.size() - 1)) {
+ throw new IndexOutOfBoundsException("Row count is " +
+ rows.size() + ", requested index " + row);
+ }
+ rows.get(row).label = label;
+ }
+
+ /**
+ * Insert one row at a particular index.
+ *
+ * @param idx the row number
+ */
+ private void insertRowAt(final int idx) {
+ Row newRow = new Row(idx);
+ for (int i = 0; i < columns.size(); i++) {
+ Cell cell = new Cell(this, columns.get(i).getX(),
+ rows.get(idx).getY(), COLUMN_DEFAULT_WIDTH, 1, i, idx);
+ newRow.add(cell);
+ columns.get(i).cells.add(idx, cell);
+ }
+ rows.add(idx, newRow);
+
+ for (int x = 0; x < columns.size(); x++) {
+ for (int y = idx; y < rows.size(); y++) {
+ columns.get(x).get(y).row = y;
+ columns.get(x).get(y).column = x;
+ }
+ }
+ for (int i = idx + 1; i < rows.size(); i++) {
+ String oldRowLabel = Integer.toString(i - 1);
+ if (rows.get(i).label.equals(oldRowLabel)) {
+ rows.get(i).label = Integer.toString(i);
+ }
+ }
+ alignGrid();
+ }
+
+ /**
+ * Insert one row above a particular row.
+ *
+ * @param row the row number
+ */
+ public void insertRowAbove(final int row) {
+ if ((row < 0) || (row > rows.size() - 1)) {
+ throw new IndexOutOfBoundsException("Row count is " +
+ rows.size() + ", requested index " + row);
+ }
+ insertRowAt(row);
+ selectedRow++;
+ activate(columns.get(selectedColumn).get(selectedRow));
+ }
+
+ /**
+ * Insert one row below a particular row.
+ *
+ * @param row the row number
+ */
+ public void insertRowBelow(final int row) {
+ if ((row < 0) || (row > rows.size() - 1)) {
+ throw new IndexOutOfBoundsException("Row count is " +
+ rows.size() + ", requested index " + row);
+ }
+ int idx = row + 1;
+ if (idx < rows.size()) {
+ insertRowAt(idx);
+ activate(columns.get(selectedColumn).get(selectedRow));
+ return;
+ }
+
+ // row is the last row, we need to perform an append.
+ Row newRow = new Row(idx);
+ for (int i = 0; i < columns.size(); i++) {
+ Cell cell = new Cell(this, columns.get(i).getX(),
+ rows.get(row).getY(), COLUMN_DEFAULT_WIDTH, 1, i, idx);
+ newRow.add(cell);
+ columns.get(i).cells.add(cell);
+ }
+ rows.add(newRow);
+ alignGrid();
+ activate(columns.get(selectedColumn).get(selectedRow));
+ }
+
+ /**
+ * Delete a particular row.
+ *
+ * @param row the row number
+ */
+ public void deleteRow(final int row) {
+ if ((row < 0) || (row > rows.size() - 1)) {
+ throw new IndexOutOfBoundsException("Row count is " +
+ rows.size() + ", requested index " + row);
+ }
+ if (rows.size() == 1) {
+ // Don't delete the last row.
+ return;
+ }
+ for (int i = 0; i < columns.size(); i++) {
+ Cell cell = columns.get(i).cells.remove(row);
+ getChildren().remove(cell);
+ }
+ rows.remove(row);
+
+ for (int x = 0; x < columns.size(); x++) {
+ for (int y = row; y < rows.size(); y++) {
+ columns.get(x).get(y).row = y;
+ columns.get(x).get(y).column = x;
+ }
+ }
+ for (int i = row; i < rows.size(); i++) {
+ String oldRowLabel = Integer.toString(i + 1);
+ if (rows.get(i).label.equals(oldRowLabel)) {
+ rows.get(i).label = Integer.toString(i);
+ }
+ }
+ if (selectedRow == rows.size()) {
+ selectedRow--;
+ }
+ activate(columns.get(selectedColumn).get(selectedRow));
+ bottomRightCorner();
+ }
+
+ /**
+ * Insert one column at a particular index.
+ *
+ * @param idx the column number
+ */
+ private void insertColumnAt(final int idx) {
+ Column newColumn = new Column(idx);
+ for (int i = 0; i < rows.size(); i++) {
+ Cell cell = new Cell(this, columns.get(idx).getX(),
+ rows.get(i).getY(), COLUMN_DEFAULT_WIDTH, 1, idx, i);
+ newColumn.add(cell);
+ rows.get(i).cells.add(idx, cell);
+ }
+ columns.add(idx, newColumn);
+
+ for (int x = idx; x < columns.size(); x++) {
+ for (int y = 0; y < rows.size(); y++) {
+ columns.get(x).get(y).row = y;
+ columns.get(x).get(y).column = x;
+ }
+ }
+ for (int i = idx + 1; i < columns.size(); i++) {
+ String oldColumnLabel = makeColumnLabel(i - 1);
+ if (columns.get(i).label.equals(oldColumnLabel)) {
+ columns.get(i).label = makeColumnLabel(i);
+ }
+ }
+ alignGrid();
+ }
+
+ /**
+ * Insert one column to the left of a particular column.
+ *
+ * @param column the column number
+ */
+ public void insertColumnLeft(final int column) {
+ if ((column < 0) || (column > columns.size() - 1)) {
+ throw new IndexOutOfBoundsException("Column count is " +
+ columns.size() + ", requested index " + column);
+ }
+ insertColumnAt(column);
+ selectedColumn++;
+ activate(columns.get(selectedColumn).get(selectedRow));
+ }
+
+ /**
+ * Insert one column to the right of a particular column.
+ *
+ * @param column the column number
+ */
+ public void insertColumnRight(final int column) {
+ if ((column < 0) || (column > columns.size() - 1)) {
+ throw new IndexOutOfBoundsException("Column count is " +
+ columns.size() + ", requested index " + column);
+ }
+ int idx = column + 1;
+ if (idx < columns.size()) {
+ insertColumnAt(idx);
+ activate(columns.get(selectedColumn).get(selectedRow));
+ return;
+ }
+
+ // column is the last column, we need to perform an append.
+ Column newColumn = new Column(idx);
+ for (int i = 0; i < rows.size(); i++) {
+ Cell cell = new Cell(this, columns.get(column).getX(),
+ rows.get(i).getY(), COLUMN_DEFAULT_WIDTH, 1, idx, i);
+ newColumn.add(cell);
+ rows.get(i).cells.add(cell);
+ }
+ columns.add(newColumn);
+ alignGrid();
+ activate(columns.get(selectedColumn).get(selectedRow));
+ }
+
+ /**
+ * Delete a particular column.
+ *
+ * @param column the column number
+ */
+ public void deleteColumn(final int column) {
+ if ((column < 0) || (column > columns.size() - 1)) {
+ throw new IndexOutOfBoundsException("Column count is " +
+ columns.size() + ", requested index " + column);
+ }
+ if (columns.size() == 1) {
+ // Don't delete the last column.
+ return;
+ }
+ for (int i = 0; i < rows.size(); i++) {
+ Cell cell = rows.get(i).cells.remove(column);
+ getChildren().remove(cell);
+ }
+ columns.remove(column);
+
+ for (int x = column; x < columns.size(); x++) {
+ for (int y = 0; y < rows.size(); y++) {
+ columns.get(x).get(y).row = y;
+ columns.get(x).get(y).column = x;
+ }
+ }
+ for (int i = column; i < columns.size(); i++) {
+ String oldColumnLabel = makeColumnLabel(i + 1);
+ if (columns.get(i).label.equals(oldColumnLabel)) {
+ columns.get(i).label = makeColumnLabel(i);
+ }
+ }
+ if (selectedColumn == columns.size()) {
+ selectedColumn--;
+ }
+ activate(columns.get(selectedColumn).get(selectedRow));
+ bottomRightCorner();
+ }
+
+ /**
+ * Delete the selected cell, shifting cells over to the left.
+ */
+ public void deleteCellShiftLeft() {
+ // All we do is copy the text from every cell in this row over.
+ for (int i = selectedColumn + 1; i < columns.size(); i++) {
+ setCellText(i - 1, selectedRow, getCellText(i, selectedRow));
+ }
+ setCellText(columns.size() - 1, selectedRow, "");
+ }
+
+ /**
+ * Delete the selected cell, shifting cells from below up.
+ */
+ public void deleteCellShiftUp() {
+ // All we do is copy the text from every cell in this column up.
+ for (int i = selectedRow + 1; i < rows.size(); i++) {
+ setCellText(selectedColumn, i - 1, getCellText(selectedColumn, i));
+ }
+ setCellText(selectedColumn, rows.size() - 1, "");
+ }
+
+ /**
+ * Set a particular cell read-only (non-editable) or not.
+ *
+ * @param column the cell column
+ * @param row the cell row
+ * @param readOnly if true, the cell will be non-editable
+ */
+ public void setCellReadOnly(final int column, final int row,
+ final boolean readOnly) {
+
+ if ((column < 0) || (column > columns.size() - 1)) {
+ throw new IndexOutOfBoundsException("Column count is " +
+ columns.size() + ", requested index " + column);
+ }
+ if ((row < 0) || (row > rows.size() - 1)) {
+ throw new IndexOutOfBoundsException("Row count is " +
+ rows.size() + ", requested index " + row);
+ }
+ rows.get(row).get(column).setReadOnly(readOnly);
+ }
+
+ /**
+ * Set an entire row of cells read-only (non-editable) or not.
+ *
+ * @param row the row number
+ * @param readOnly if true, the cells will be non-editable
+ */
+ public void setRowReadOnly(final int row, final boolean readOnly) {
+ if ((row < 0) || (row > rows.size() - 1)) {
+ throw new IndexOutOfBoundsException("Row count is " +
+ rows.size() + ", requested index " + row);
+ }
+ for (Cell cell: rows.get(row).cells) {
+ cell.setReadOnly(readOnly);
+ }
+ }
+
+ /**
+ * Set an entire column of cells read-only (non-editable) or not.
+ *
+ * @param column the column number
+ * @param readOnly if true, the cells will be non-editable
+ */
+ public void setColumnReadOnly(final int column, final boolean readOnly) {
+ if ((column < 0) || (column > columns.size() - 1)) {
+ throw new IndexOutOfBoundsException("Column count is " +
+ columns.size() + ", requested index " + column);
+ }
+ for (Cell cell: columns.get(column).cells) {
+ cell.setReadOnly(readOnly);
+ }
+ }
+
+ /**
+ * Set all borders across the entire table to Border.NONE.
+ */
+ public void setBorderAllNone() {
+ topBorder = Border.NONE;
+ leftBorder = Border.NONE;
+ for (int i = 0; i < columns.size(); i++) {
+ columns.get(i).rightBorder = Border.NONE;
+ }
+ for (int i = 0; i < rows.size(); i++) {
+ rows.get(i).bottomBorder = Border.NONE;
+ rows.get(i).height = 1;
+ }
+ bottomRightCorner();
+ }
+
+ /**
+ * Set all borders across the entire table to Border.SINGLE.
+ */
+ public void setBorderAllSingle() {
+ topBorder = Border.SINGLE;
+ leftBorder = Border.SINGLE;
+ for (int i = 0; i < columns.size(); i++) {
+ columns.get(i).rightBorder = Border.SINGLE;
+ }
+ for (int i = 0; i < rows.size(); i++) {
+ rows.get(i).bottomBorder = Border.SINGLE;
+ rows.get(i).height = 2;
+ }
+ alignGrid();
+ }
+
+ /**
+ * Set all borders around the selected cell to Border.NONE.
+ */
+ public void setBorderCellNone() {
+ if (selectedRow == 0) {
+ topBorder = Border.NONE;
+ }
+ if (selectedColumn == 0) {
+ leftBorder = Border.NONE;
+ }
+ if (selectedColumn > 0) {
+ columns.get(selectedColumn - 1).rightBorder = Border.NONE;
+ }
+ columns.get(selectedColumn).rightBorder = Border.NONE;
+ if (selectedRow > 0) {
+ rows.get(selectedRow - 1).bottomBorder = Border.NONE;
+ rows.get(selectedRow - 1).height = 1;
+ }
+ rows.get(selectedRow).bottomBorder = Border.NONE;
+ rows.get(selectedRow).height = 1;
+ bottomRightCorner();
+ }
+
+ /**
+ * Set all borders around the selected cell to Border.SINGLE.
+ */
+ public void setBorderCellSingle() {
+ if (selectedRow == 0) {
+ topBorder = Border.SINGLE;
+ }
+ if (selectedColumn == 0) {
+ leftBorder = Border.SINGLE;
+ }
+ if (selectedColumn > 0) {
+ columns.get(selectedColumn - 1).rightBorder = Border.SINGLE;
+ }
+ columns.get(selectedColumn).rightBorder = Border.SINGLE;
+ if (selectedRow > 0) {
+ rows.get(selectedRow - 1).bottomBorder = Border.SINGLE;
+ rows.get(selectedRow - 1).height = 2;
+ }
+ rows.get(selectedRow).bottomBorder = Border.SINGLE;
+ rows.get(selectedRow).height = 2;
+ alignGrid();
+ }
+
+ /**
+ * Set the column border to the right of the selected cell to
+ * Border.SINGLE.
+ */
+ public void setBorderColumnRightSingle() {
+ columns.get(selectedColumn).rightBorder = Border.SINGLE;
+ alignGrid();
+ }
+
+ /**
+ * Set the column border to the right of the selected cell to
+ * Border.SINGLE.
+ */
+ public void setBorderColumnLeftSingle() {
+ if (selectedColumn == 0) {
+ leftBorder = Border.SINGLE;
+ } else {
+ columns.get(selectedColumn - 1).rightBorder = Border.SINGLE;
+ }
+ alignGrid();
+ }
+
+ /**
+ * Set the row border above the selected cell to Border.SINGLE.
+ */
+ public void setBorderRowAboveSingle() {
+ if (selectedRow == 0) {
+ topBorder = Border.SINGLE;
+ } else {
+ rows.get(selectedRow - 1).bottomBorder = Border.SINGLE;
+ rows.get(selectedRow - 1).height = 2;
+ }
+ alignGrid();
+ }
+
+ /**
+ * Set the row border below the selected cell to Border.SINGLE.
+ */
+ public void setBorderRowBelowSingle() {
+ rows.get(selectedRow).bottomBorder = Border.SINGLE;
+ rows.get(selectedRow).height = 2;
+ alignGrid();
+ }
+
+ /**
+ * Set the row border below the selected cell to Border.DOUBLE.
+ */
+ public void setBorderRowBelowDouble() {
+ rows.get(selectedRow).bottomBorder = Border.DOUBLE;
+ rows.get(selectedRow).height = 2;
+ alignGrid();
+ }
+
+ /**
+ * Set the row border below the selected cell to Border.THICK.
+ */
+ public void setBorderRowBelowThick() {
+ rows.get(selectedRow).bottomBorder = Border.THICK;
+ rows.get(selectedRow).height = 2;
+ alignGrid();
+ }
+
+ }
--- /dev/null
+ /*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+ package jexer;
+
+ import java.io.File;
+ import java.io.IOException;
+ import java.text.MessageFormat;
+ import java.util.ResourceBundle;
+
+ 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 jexer.menu.TMenuItem;
+ import static jexer.TCommand.*;
+ import static jexer.TKeypress.*;
+
+ /**
+ * TTableWindow is used to display and edit regular two-dimensional tables of
+ * cells.
+ */
+ public class TTableWindow extends TScrollableWindow {
+
+ /**
+ * Translated strings.
+ */
+ private static final ResourceBundle i18n = ResourceBundle.getBundle(TTableWindow.class.getName());
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The table widget.
+ */
+ private TTableWidget tableField;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor sets window title.
+ *
+ * @param parent the main application
+ * @param title the window title
+ */
+ public TTableWindow(final TApplication parent, final String title) {
+
+ super(parent, title, 0, 0, parent.getScreen().getWidth() / 2,
+ parent.getScreen().getHeight() / 2 - 2, RESIZABLE | CENTERED);
+
+ tableField = addTable(0, 0, getWidth() - 2, getHeight() - 2);
+ setupAfterTable();
+ }
+
+ /**
+ * Public constructor loads a grid from a RFC4180 CSV file.
+ *
+ * @param parent the main application
+ * @param csvFile a File referencing the CSV data
+ * @throws IOException if a java.io operation throws
+ */
+ public TTableWindow(final TApplication parent,
+ final File csvFile) throws IOException {
+
+ super(parent, csvFile.getName(), 0, 0,
+ parent.getScreen().getWidth() / 2,
+ parent.getScreen().getHeight() / 2 - 2,
+ RESIZABLE | CENTERED);
+
+ tableField = addTable(0, 0, getWidth() - 2, getHeight() - 2, 1, 1);
+ setupAfterTable();
+ tableField.loadCsvFile(csvFile);
+ }
+
+ // ------------------------------------------------------------------------
+ // Event handlers ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Called by application.switchWindow() when this window gets the
+ * focus, and also by application.addWindow().
+ */
+ 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);
+ getApplication().enableMenuItem(TMenu.MID_TABLE_VIEW_COLUMN_LABELS);
+ getApplication().enableMenuItem(TMenu.MID_TABLE_VIEW_HIGHLIGHT_ROW);
+ getApplication().enableMenuItem(TMenu.MID_TABLE_VIEW_HIGHLIGHT_COLUMN);
+ getApplication().enableMenuItem(TMenu.MID_TABLE_BORDER_NONE);
+ getApplication().enableMenuItem(TMenu.MID_TABLE_BORDER_ALL);
+ getApplication().enableMenuItem(TMenu.MID_TABLE_BORDER_CELL_NONE);
+ getApplication().enableMenuItem(TMenu.MID_TABLE_BORDER_CELL_ALL);
+ getApplication().enableMenuItem(TMenu.MID_TABLE_BORDER_RIGHT);
+ getApplication().enableMenuItem(TMenu.MID_TABLE_BORDER_LEFT);
+ getApplication().enableMenuItem(TMenu.MID_TABLE_BORDER_TOP);
+ getApplication().enableMenuItem(TMenu.MID_TABLE_BORDER_BOTTOM);
+ getApplication().enableMenuItem(TMenu.MID_TABLE_BORDER_DOUBLE_BOTTOM);
+ getApplication().enableMenuItem(TMenu.MID_TABLE_BORDER_THICK_BOTTOM);
+ getApplication().enableMenuItem(TMenu.MID_TABLE_DELETE_LEFT);
+ getApplication().enableMenuItem(TMenu.MID_TABLE_DELETE_UP);
+ getApplication().enableMenuItem(TMenu.MID_TABLE_DELETE_ROW);
+ getApplication().enableMenuItem(TMenu.MID_TABLE_DELETE_COLUMN);
+ getApplication().enableMenuItem(TMenu.MID_TABLE_INSERT_LEFT);
+ getApplication().enableMenuItem(TMenu.MID_TABLE_INSERT_RIGHT);
+ getApplication().enableMenuItem(TMenu.MID_TABLE_INSERT_ABOVE);
+ getApplication().enableMenuItem(TMenu.MID_TABLE_INSERT_BELOW);
+ getApplication().enableMenuItem(TMenu.MID_TABLE_COLUMN_NARROW);
+ getApplication().enableMenuItem(TMenu.MID_TABLE_COLUMN_WIDEN);
+ getApplication().enableMenuItem(TMenu.MID_TABLE_FILE_OPEN_CSV);
+ getApplication().enableMenuItem(TMenu.MID_TABLE_FILE_SAVE_CSV);
+ getApplication().enableMenuItem(TMenu.MID_TABLE_FILE_SAVE_TEXT);
+
+ if (tableField != null) {
+
+ // Set the menu to match the flags.
+ TMenuItem menuItem = getApplication().getMenuItem(TMenu.MID_TABLE_VIEW_ROW_LABELS);
+ if (menuItem != null) {
+ menuItem.setChecked(tableField.getShowRowLabels());
+ }
+ menuItem = getApplication().getMenuItem(TMenu.MID_TABLE_VIEW_COLUMN_LABELS);
+ if (menuItem != null) {
+ menuItem.setChecked(tableField.getShowColumnLabels());
+ }
+ menuItem = getApplication().getMenuItem(TMenu.MID_TABLE_VIEW_HIGHLIGHT_ROW);
+ if (menuItem != null) {
+ menuItem.setChecked(tableField.getHighlightRow());
+ }
+ menuItem = getApplication().getMenuItem(TMenu.MID_TABLE_VIEW_HIGHLIGHT_COLUMN);
+ if (menuItem != null) {
+ menuItem.setChecked(tableField.getHighlightColumn());
+ }
+ }
+ }
+
+ /**
+ * Called by application.switchWindow() when another window gets the
+ * focus.
+ */
+ 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);
+ getApplication().disableMenuItem(TMenu.MID_TABLE_VIEW_COLUMN_LABELS);
+ getApplication().disableMenuItem(TMenu.MID_TABLE_VIEW_HIGHLIGHT_ROW);
+ getApplication().disableMenuItem(TMenu.MID_TABLE_VIEW_HIGHLIGHT_COLUMN);
+ getApplication().disableMenuItem(TMenu.MID_TABLE_BORDER_NONE);
+ getApplication().disableMenuItem(TMenu.MID_TABLE_BORDER_ALL);
+ getApplication().disableMenuItem(TMenu.MID_TABLE_BORDER_CELL_NONE);
+ getApplication().disableMenuItem(TMenu.MID_TABLE_BORDER_CELL_ALL);
+ getApplication().disableMenuItem(TMenu.MID_TABLE_BORDER_RIGHT);
+ getApplication().disableMenuItem(TMenu.MID_TABLE_BORDER_LEFT);
+ getApplication().disableMenuItem(TMenu.MID_TABLE_BORDER_TOP);
+ getApplication().disableMenuItem(TMenu.MID_TABLE_BORDER_BOTTOM);
+ getApplication().disableMenuItem(TMenu.MID_TABLE_BORDER_DOUBLE_BOTTOM);
+ getApplication().disableMenuItem(TMenu.MID_TABLE_BORDER_THICK_BOTTOM);
+ getApplication().disableMenuItem(TMenu.MID_TABLE_DELETE_LEFT);
+ getApplication().disableMenuItem(TMenu.MID_TABLE_DELETE_UP);
+ getApplication().disableMenuItem(TMenu.MID_TABLE_DELETE_ROW);
+ getApplication().disableMenuItem(TMenu.MID_TABLE_DELETE_COLUMN);
+ getApplication().disableMenuItem(TMenu.MID_TABLE_INSERT_LEFT);
+ getApplication().disableMenuItem(TMenu.MID_TABLE_INSERT_RIGHT);
+ getApplication().disableMenuItem(TMenu.MID_TABLE_INSERT_ABOVE);
+ getApplication().disableMenuItem(TMenu.MID_TABLE_INSERT_BELOW);
+ getApplication().disableMenuItem(TMenu.MID_TABLE_COLUMN_NARROW);
+ getApplication().disableMenuItem(TMenu.MID_TABLE_COLUMN_WIDEN);
+ getApplication().disableMenuItem(TMenu.MID_TABLE_FILE_OPEN_CSV);
+ getApplication().disableMenuItem(TMenu.MID_TABLE_FILE_SAVE_CSV);
+ getApplication().disableMenuItem(TMenu.MID_TABLE_FILE_SAVE_TEXT);
+ }
+
+ // ------------------------------------------------------------------------
+ // TWindow ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Handle mouse press events.
+ *
+ * @param mouse mouse button press event
+ */
+ @Override
+ public void onMouseDown(final TMouseEvent mouse) {
+ // Use TWidget's code to pass the event to the children.
+ super.onMouseDown(mouse);
+
+ if (mouseOnTable(mouse)) {
+ // The table might have changed, update the scollbars.
+ setBottomValue(tableField.getRowCount() - 1);
+ setVerticalValue(tableField.getSelectedRowNumber());
+ setRightValue(tableField.getColumnCount() - 1);
+ setHorizontalValue(tableField.getSelectedColumnNumber());
+ }
+ }
+
+ /**
+ * Handle mouse release events.
+ *
+ * @param mouse mouse button release event
+ */
+ @Override
+ public void onMouseUp(final TMouseEvent mouse) {
+ // Use TWidget's code to pass the event to the children.
+ super.onMouseUp(mouse);
+
+ if (mouse.isMouse1() && mouseOnVerticalScroller(mouse)) {
+ // Clicked/dragged on vertical scrollbar.
+ tableField.setSelectedRowNumber(getVerticalValue());
+ }
+ if (mouse.isMouse1() && mouseOnHorizontalScroller(mouse)) {
+ // Clicked/dragged on horizontal scrollbar.
+ tableField.setSelectedColumnNumber(getHorizontalValue());
+ }
+ }
+
+ /**
+ * Method that subclasses can override to handle mouse movements.
+ *
+ * @param mouse mouse motion event
+ */
+ @Override
+ public void onMouseMotion(final TMouseEvent mouse) {
+ // Use TWidget's code to pass the event to the children.
+ super.onMouseMotion(mouse);
+
+ if (mouseOnTable(mouse) && mouse.isMouse1()) {
+ // The table might have changed, update the scollbars.
+ setBottomValue(tableField.getRowCount() - 1);
+ setVerticalValue(tableField.getSelectedRowNumber());
+ setRightValue(tableField.getColumnCount() - 1);
+ setHorizontalValue(tableField.getSelectedColumnNumber());
+ } else {
+ if (mouse.isMouse1() && mouseOnVerticalScroller(mouse)) {
+ // Clicked/dragged on vertical scrollbar.
+ tableField.setSelectedRowNumber(getVerticalValue());
+ }
+ if (mouse.isMouse1() && mouseOnHorizontalScroller(mouse)) {
+ // Clicked/dragged on horizontal scrollbar.
+ tableField.setSelectedColumnNumber(getHorizontalValue());
+ }
+ }
+
+ }
+
+ /**
+ * Handle keystrokes.
+ *
+ * @param keypress keystroke event
+ */
+ @Override
+ public void onKeypress(final TKeypressEvent keypress) {
+ // Use TWidget's code to pass the event to the children.
+ super.onKeypress(keypress);
+
+ // The table might have changed, update the scollbars.
+ setBottomValue(tableField.getRowCount() - 1);
+ setVerticalValue(tableField.getSelectedRowNumber());
+ setRightValue(tableField.getColumnCount() - 1);
+ setHorizontalValue(tableField.getSelectedColumnNumber());
+ }
+
+ /**
+ * Handle window/screen resize events.
+ *
+ * @param event resize event
+ */
+ @Override
+ public void onResize(final TResizeEvent event) {
+ if (event.getType() == TResizeEvent.Type.WIDGET) {
+ // Resize the table
+ TResizeEvent tableSize = new TResizeEvent(TResizeEvent.Type.WIDGET,
+ event.getWidth() - 2, event.getHeight() - 2);
+ tableField.onResize(tableSize);
+
+ // Have TScrollableWindow handle the scrollbars
+ super.onResize(event);
+ return;
+ }
+
+ // Pass to children instead
+ for (TWidget widget: getChildren()) {
+ widget.onResize(event);
+ }
+ }
+
+ /**
+ * Method that subclasses can override to handle posted command events.
+ *
+ * @param command command event
+ */
+ @Override
+ public void onCommand(final TCommandEvent command) {
+ if (command.equals(cmOpen)) {
+ try {
+ String filename = fileOpenBox(".");
+ if (filename != null) {
+ try {
+ new TTableWindow(getApplication(), new File(filename));
+ } catch (IOException e) {
+ messageBox(i18n.getString("errorDialogTitle"),
+ MessageFormat.format(i18n.
+ getString("errorReadingFile"), e.getMessage()));
+ }
+ }
+ } catch (IOException e) {
+ messageBox(i18n.getString("errorDialogTitle"),
+ MessageFormat.format(i18n.
+ getString("errorOpeningFileDialog"), e.getMessage()));
+ }
+ return;
+ }
+
+ if (command.equals(cmSave)) {
+ try {
+ String filename = fileSaveBox(".");
+ if (filename != null) {
+ tableField.saveToCsvFilename(filename);
+ }
+ } catch (IOException e) {
+ messageBox(i18n.getString("errorDialogTitle"),
+ MessageFormat.format(i18n.
+ getString("errorWritingFile"), e.getMessage()));
+ }
+ return;
+ }
+
+ // Didn't handle it, let children get it instead
+ super.onCommand(command);
+ }
+
+ /**
+ * Handle posted menu events.
+ *
+ * @param menu menu event
+ */
+ @Override
+ public void onMenu(final TMenuEvent menu) {
+ TInputBox inputBox = null;
+ String filename = null;
+
+ switch (menu.getId()) {
+ case TMenu.MID_TABLE_RENAME_COLUMN:
+ inputBox = inputBox(i18n.getString("renameColumnInputTitle"),
+ i18n.getString("renameColumnInputCaption"),
+ tableField.getColumnLabel(tableField.getSelectedColumnNumber()),
+ TMessageBox.Type.OKCANCEL);
+ if (inputBox.isOk()) {
+ tableField.setColumnLabel(tableField.getSelectedColumnNumber(),
+ inputBox.getText());
+ }
+ return;
+ case TMenu.MID_TABLE_RENAME_ROW:
+ inputBox = inputBox(i18n.getString("renameRowInputTitle"),
+ i18n.getString("renameRowInputCaption"),
+ tableField.getRowLabel(tableField.getSelectedRowNumber()),
+ TMessageBox.Type.OKCANCEL);
+ if (inputBox.isOk()) {
+ tableField.setRowLabel(tableField.getSelectedRowNumber(),
+ inputBox.getText());
+ }
+ return;
+ case TMenu.MID_TABLE_VIEW_ROW_LABELS:
+ tableField.setShowRowLabels(getApplication().getMenuItem(
+ menu.getId()).getChecked());
+ return;
+ case TMenu.MID_TABLE_VIEW_COLUMN_LABELS:
+ tableField.setShowColumnLabels(getApplication().getMenuItem(
+ menu.getId()).getChecked());
+ return;
+ case TMenu.MID_TABLE_VIEW_HIGHLIGHT_ROW:
+ tableField.setHighlightRow(getApplication().getMenuItem(
+ menu.getId()).getChecked());
+ return;
+ case TMenu.MID_TABLE_VIEW_HIGHLIGHT_COLUMN:
+ tableField.setHighlightColumn(getApplication().getMenuItem(
+ menu.getId()).getChecked());
+ return;
+ case TMenu.MID_TABLE_BORDER_NONE:
+ tableField.setBorderAllNone();
+ return;
+ case TMenu.MID_TABLE_BORDER_ALL:
+ tableField.setBorderAllSingle();
+ return;
+ case TMenu.MID_TABLE_BORDER_CELL_NONE:
+ tableField.setBorderCellNone();
+ return;
+ case TMenu.MID_TABLE_BORDER_CELL_ALL:
+ tableField.setBorderCellSingle();
+ return;
+ case TMenu.MID_TABLE_BORDER_RIGHT:
+ tableField.setBorderColumnRightSingle();
+ return;
+ case TMenu.MID_TABLE_BORDER_LEFT:
+ tableField.setBorderColumnLeftSingle();
+ return;
+ case TMenu.MID_TABLE_BORDER_TOP:
+ tableField.setBorderRowAboveSingle();
+ return;
+ case TMenu.MID_TABLE_BORDER_BOTTOM:
+ tableField.setBorderRowBelowSingle();
+ return;
+ case TMenu.MID_TABLE_BORDER_DOUBLE_BOTTOM:
+ tableField.setBorderRowBelowDouble();
+ return;
+ case TMenu.MID_TABLE_BORDER_THICK_BOTTOM:
+ tableField.setBorderRowBelowThick();
+ return;
+ case TMenu.MID_TABLE_DELETE_LEFT:
+ tableField.deleteCellShiftLeft();
+ return;
+ case TMenu.MID_TABLE_DELETE_UP:
+ tableField.deleteCellShiftUp();
+ return;
+ case TMenu.MID_TABLE_DELETE_ROW:
+ tableField.deleteRow(tableField.getSelectedRowNumber());
+ return;
+ case TMenu.MID_TABLE_DELETE_COLUMN:
+ tableField.deleteColumn(tableField.getSelectedColumnNumber());
+ return;
+ case TMenu.MID_TABLE_INSERT_LEFT:
+ tableField.insertColumnLeft(tableField.getSelectedColumnNumber());
+ return;
+ case TMenu.MID_TABLE_INSERT_RIGHT:
+ tableField.insertColumnRight(tableField.getSelectedColumnNumber());
+ return;
+ case TMenu.MID_TABLE_INSERT_ABOVE:
+ tableField.insertRowAbove(tableField.getSelectedColumnNumber());
+ return;
+ case TMenu.MID_TABLE_INSERT_BELOW:
+ tableField.insertRowBelow(tableField.getSelectedColumnNumber());
+ return;
+ case TMenu.MID_TABLE_COLUMN_NARROW:
+ tableField.setColumnWidth(tableField.getSelectedColumnNumber(),
+ tableField.getColumnWidth(tableField.getSelectedColumnNumber()) - 1);
+ return;
+ case TMenu.MID_TABLE_COLUMN_WIDEN:
+ tableField.setColumnWidth(tableField.getSelectedColumnNumber(),
+ tableField.getColumnWidth(tableField.getSelectedColumnNumber()) + 1);
+ return;
+ case TMenu.MID_TABLE_FILE_OPEN_CSV:
+ try {
+ filename = fileOpenBox(".");
+ if (filename != null) {
+ try {
+ new TTableWindow(getApplication(), new File(filename));
+ } catch (IOException e) {
+ messageBox(i18n.getString("errorDialogTitle"),
+ MessageFormat.format(i18n.
+ getString("errorReadingFile"), e.getMessage()));
+ }
+ }
+ } catch (IOException e) {
+ messageBox(i18n.getString("errorDialogTitle"),
+ MessageFormat.format(i18n.
+ getString("errorOpeningFileDialog"), e.getMessage()));
+ }
+ return;
+ case TMenu.MID_TABLE_FILE_SAVE_CSV:
+ try {
+ filename = fileSaveBox(".");
+ if (filename != null) {
+ tableField.saveToCsvFilename(filename);
+ }
+ } catch (IOException e) {
+ messageBox(i18n.getString("errorDialogTitle"),
+ MessageFormat.format(i18n.
+ getString("errorWritingFile"), e.getMessage()));
+ }
+ return;
+ case TMenu.MID_TABLE_FILE_SAVE_TEXT:
+ try {
+ filename = fileSaveBox(".");
+ if (filename != null) {
+ tableField.saveToTextFilename(filename);
+ }
+ } catch (IOException e) {
+ messageBox(i18n.getString("errorDialogTitle"),
+ MessageFormat.format(i18n.
+ getString("errorWritingFile"), e.getMessage()));
+ }
+ return;
+ default:
+ break;
+ }
+
+ super.onMenu(menu);
+ }
+
+ // ------------------------------------------------------------------------
+ // TTableWindow -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Setup other fields after the table is created.
+ */
+ private void setupAfterTable() {
+ hScroller = new THScroller(this, 17, getHeight() - 2, getWidth() - 20);
+ vScroller = new TVScroller(this, getWidth() - 2, 0, getHeight() - 2);
+ setMinimumWindowWidth(25);
+ setMinimumWindowHeight(10);
+ setTopValue(tableField.getSelectedRowNumber());
+ setBottomValue(tableField.getRowCount() - 1);
+ setLeftValue(tableField.getSelectedColumnNumber());
+ setRightValue(tableField.getColumnCount() - 1);
+
+ statusBar = newStatusBar(i18n.getString("statusBar"));
+ statusBar.addShortcutKeypress(kbF1, cmHelp,
+ i18n.getString("statusBarHelp"));
+
+ statusBar.addShortcutKeypress(kbF2, cmSave,
+ i18n.getString("statusBarSave"));
+ statusBar.addShortcutKeypress(kbF3, cmOpen,
+ i18n.getString("statusBarOpen"));
+ statusBar.addShortcutKeypress(kbF10, cmMenu,
+ i18n.getString("statusBarMenu"));
+
+ // Synchronize the menu with tableField's flags.
+ onFocus();
+ }
+
+ /**
+ * Check if a mouse press/release/motion event coordinate is over the
+ * table.
+ *
+ * @param mouse a mouse-based event
+ * @return whether or not the mouse is on the table
+ */
+ private boolean mouseOnTable(final TMouseEvent mouse) {
+ if ((mouse.getAbsoluteX() >= getAbsoluteX() + 1)
+ && (mouse.getAbsoluteX() < getAbsoluteX() + getWidth() - 1)
+ && (mouse.getAbsoluteY() >= getAbsoluteY() + 1)
+ && (mouse.getAbsoluteY() < getAbsoluteY() + getHeight() - 1)
+ ) {
+ return true;
+ }
+ return false;
+ }
+
+ }
--- /dev/null
+ statusBar=Editor
+ statusBarHelp=Help
+ statusBarSave=Save CSV
+ statusBarOpen=Open CSV
+ statusBarMenu=Menu
+
+ renameRowInputTitle=Rename Row
+ renameRowInputCaption=New row name?
+ renameColumnInputTitle=Rename Column
+ renameColumnInputCaption=New column name?
+
+ errorDialogTitle=Error
+ errorReadingFile=Error reading file: {0}
+ errorOpeningFileDialog=Error opening file dialog: {0}
+ errorSavingFile=Error saving file: {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;
+
+ 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.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.TKeypress.*;
+
+ /**
+ * TTerminalWidget exposes a ECMA-48 / ANSI X3.64 style terminal in a widget.
+ */
+ public class TTerminalWidget extends TScrollableWidget
+ implements DisplayListener {
+
+ /**
+ * Translated strings.
+ */
+ private static final ResourceBundle i18n = ResourceBundle.getBundle(TTerminalWidget.class.getName());
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The emulator.
+ */
+ private ECMA48 emulator;
+
+ /**
+ * The Process created by the shell spawning constructor.
+ */
+ private Process shell;
+
+ /**
+ * If true, we are using the ptypipe utility to support dynamic window
+ * resizing. ptypipe is available at
+ * https://gitlab.com/klamonte/ptypipe .
+ */
+ private boolean ptypipe = false;
+
+ /**
+ * Double-height font.
+ */
+ private GlyphMaker doubleFont;
+
+ /**
+ * Last text width value.
+ */
+ private int lastTextWidth = -1;
+
+ /**
+ * Last text height value.
+ */
+ private int lastTextHeight = -1;
+
+ /**
+ * The blink state, used only by ECMA48 backend and when double-width
+ * chars must be drawn.
+ */
+ private boolean blinkState = true;
+
+ /**
+ * Timer flag, used only by ECMA48 backend and when double-width chars
+ * must be drawn.
+ */
+ private boolean haveTimer = false;
+
+ /**
+ * The last seen visible display.
+ */
+ private List<DisplayLine> display;
+
+ /**
+ * If true, the display has changed and needs updating.
+ */
+ private volatile boolean dirty = true;
+
+ /**
+ * Time that the display was last updated.
+ */
+ private long lastUpdateTime = 0;
+
+ /**
+ * If true, hide the mouse after typing a keystroke.
+ */
+ private boolean hideMouseWhenTyping = true;
+
+ /**
+ * If true, the mouse should not be displayed because a keystroke was
+ * typed.
+ */
+ private boolean typingHidMouse = false;
+
+ /**
+ * The return value from the emulator.
+ */
+ private int exitValue = -1;
+
+ /**
+ * Title to expose to a window.
+ */
+ private String title = "";
+
+ /**
+ * Action to perform when the terminal exits.
+ */
+ private TAction closeAction = null;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor spawns a custom command line.
+ *
+ * @param parent parent widget
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param commandLine the command line to execute
+ */
+ public TTerminalWidget(final TWidget parent, final int x, final int y,
+ final String commandLine) {
+
+ this(parent, x, y, commandLine.split("\\s+"));
+ }
+
+ /**
+ * Public constructor spawns a custom command line.
+ *
+ * @param parent parent widget
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param command the command line to execute
+ */
+ public TTerminalWidget(final TWidget parent, final int x, final int y,
+ final String [] command) {
+
+ this(parent, x, y, command, null);
+ }
+
+ /**
+ * Public constructor spawns a custom command line.
+ *
+ * @param parent parent widget
+ * @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
+ */
+ public TTerminalWidget(final TWidget parent, final int x, final int y,
+ final String [] command, final TAction closeAction) {
+
+ this(parent, x, y, 80, 24, command, closeAction);
+ }
+
+ /**
+ * Public constructor spawns a custom command line.
+ *
+ * @param parent parent widget
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @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
+ */
+ public TTerminalWidget(final TWidget parent, final int x, final int y,
+ final int width, final int height, final String [] command,
+ final TAction closeAction) {
+
+ super(parent, x, y, width, height);
+
+ this.closeAction = closeAction;
+
+ String [] fullCommand;
+
+ // Spawn a shell and pass its I/O to the other constructor.
+ if ((System.getProperty("jexer.TTerminal.ptypipe") != null)
+ && (System.getProperty("jexer.TTerminal.ptypipe").
+ equals("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[1] = "/c";
+ fullCommand[2] = stringArrayToString(command);
+ } else if (System.getProperty("os.name").startsWith("Mac")) {
+ fullCommand = new String[6];
+ fullCommand[0] = "script";
+ fullCommand[1] = "-q";
+ fullCommand[2] = "-F";
+ fullCommand[3] = "/dev/null";
+ fullCommand[4] = "-c";
+ 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);
+ }
+ spawnShell(fullCommand);
+ }
+
+ /**
+ * Public constructor spawns a shell.
+ *
+ * @param parent parent widget
+ * @param x column relative to parent
+ * @param y row relative to parent
+ */
+ public TTerminalWidget(final TWidget parent, final int x, final int y) {
+ this(parent, x, y, (TAction) null);
+ }
+
+ /**
+ * Public constructor spawns a shell.
+ *
+ * @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
+ */
+ public TTerminalWidget(final TWidget parent, final int x, final int y,
+ final TAction closeAction) {
+
+ this(parent, x, y, 80, 24, closeAction);
+ }
+
+ /**
+ * Public constructor spawns a shell.
+ *
+ * @param parent parent widget
+ * @param x column relative to parent
+ * @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
+ */
+ public TTerminalWidget(final TWidget parent, final int x, final int y,
+ final int width, final int height, final TAction closeAction) {
+
+ super(parent, x, y, width, height);
+
+ this.closeAction = closeAction;
+
+ if (System.getProperty("jexer.TTerminal.shell") != null) {
+ String shell = System.getProperty("jexer.TTerminal.shell");
+ if (shell.trim().startsWith("ptypipe")) {
+ ptypipe = true;
+ }
+ spawnShell(shell.split("\\s+"));
+ return;
+ }
+
+ String cmdShellWindows = "cmd.exe";
+
+ // You cannot run a login shell in a bare Process interactively, due
+ // to libc's behavior of buffering when stdin/stdout aren't a tty.
+ // Use 'script' instead to run a shell in a pty. And because BSD and
+ // GNU differ on the '-f' vs '-F' flags, we need two different
+ // commands. Lovely.
+ String cmdShellGNU = "script -fqe /dev/null";
+ String cmdShellBSD = "script -q -F /dev/null";
+
+ // ptypipe is another solution that permits dynamic window resizing.
+ String cmdShellPtypipe = "ptypipe /bin/bash --login";
+
+ // Spawn a shell and pass its I/O to the other constructor.
+ if ((System.getProperty("jexer.TTerminal.ptypipe") != null)
+ && (System.getProperty("jexer.TTerminal.ptypipe").
+ equals("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+"));
+ } else {
+ // When all else fails, assume GNU.
+ spawnShell(cmdShellGNU.split("\\s+"));
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // Event handlers ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Handle window/screen resize events.
+ *
+ * @param resize resize event
+ */
+ @Override
+ public void onResize(final TResizeEvent resize) {
+ // Let TWidget set my size.
+ super.onResize(resize);
+
+ if (emulator == null) {
+ return;
+ }
+
+ // Synchronize against the emulator so we don't stomp on its reader
+ // thread.
+ synchronized (emulator) {
+
+ if (resize.getType() == TResizeEvent.Type.WIDGET) {
+ // Resize the scroll bars
+ reflowData();
+ placeScrollbars();
+
+ // Get out of scrollback
+ setVerticalValue(0);
+
+ if (ptypipe) {
+ emulator.setWidth(getWidth());
+ emulator.setHeight(getHeight());
+
+ emulator.writeRemote("\033[8;" + getHeight() + ";" +
+ getWidth() + "t");
+ }
+
+ // Pass the correct text cell width/height to the emulator
+ if (getScreen() != null) {
+ emulator.setTextWidth(getScreen().getTextWidth());
+ emulator.setTextHeight(getScreen().getTextHeight());
+ }
+ }
+ return;
+
+ } // synchronized (emulator)
+ }
+
+ /**
+ * Handle keystrokes.
+ *
+ * @param keypress keystroke event
+ */
+ @Override
+ public void onKeypress(final TKeypressEvent keypress) {
+ if (hideMouseWhenTyping) {
+ typingHidMouse = true;
+ }
+
+ // Scrollback up/down
+ if (keypress.equals(kbShiftPgUp)
+ || keypress.equals(kbCtrlPgUp)
+ || keypress.equals(kbAltPgUp)
+ ) {
+ bigVerticalDecrement();
+ dirty = true;
+ return;
+ }
+ if (keypress.equals(kbShiftPgDn)
+ || keypress.equals(kbCtrlPgDn)
+ || keypress.equals(kbAltPgDn)
+ ) {
+ bigVerticalIncrement();
+ dirty = true;
+ return;
+ }
+
+ if ((emulator != null) && (emulator.isReading())) {
+ // Get out of scrollback
+ setVerticalValue(0);
+ emulator.addUserEvent(keypress);
+
+ // UGLY HACK TIME! cmd.exe needs CRLF, not just CR, so if
+ // this is kBEnter then also send kbCtrlJ.
+ if (keypress.equals(kbEnter)) {
+ if (System.getProperty("os.name").startsWith("Windows")
+ && (System.getProperty("jexer.TTerminal.cmdHack",
+ "true").equals("true"))
+ ) {
+ emulator.addUserEvent(new TKeypressEvent(kbCtrlJ));
+ }
+ }
+
+ readEmulatorState();
+ return;
+ }
+
+ // Process is closed, honor "normal" TUI keystrokes
+ super.onKeypress(keypress);
+ }
+
+ /**
+ * Handle mouse press events.
+ *
+ * @param mouse mouse button press event
+ */
+ @Override
+ public void onMouseDown(final TMouseEvent mouse) {
+ if (hideMouseWhenTyping) {
+ typingHidMouse = false;
+ }
+
+ if (emulator != null) {
+ // If the emulator is tracking mouse buttons, it needs to see
+ // wheel events.
+ if (emulator.getMouseProtocol() == ECMA48.MouseProtocol.OFF) {
+ if (mouse.isMouseWheelUp()) {
+ verticalDecrement();
+ dirty = true;
+ return;
+ }
+ if (mouse.isMouseWheelDown()) {
+ verticalIncrement();
+ dirty = true;
+ return;
+ }
+ }
+ if (mouseOnEmulator(mouse)) {
+ emulator.addUserEvent(mouse);
+ readEmulatorState();
+ return;
+ }
+ }
+
+ // Emulator didn't consume it, pass it on
+ super.onMouseDown(mouse);
+ }
+
+ /**
+ * Handle mouse release events.
+ *
+ * @param mouse mouse button release event
+ */
+ @Override
+ public void onMouseUp(final TMouseEvent mouse) {
+ if (hideMouseWhenTyping) {
+ typingHidMouse = false;
+ }
+
+ if ((emulator != null) && (mouseOnEmulator(mouse))) {
+ emulator.addUserEvent(mouse);
+ readEmulatorState();
+ return;
+ }
+
+ // Emulator didn't consume it, pass it on
+ super.onMouseUp(mouse);
+ }
+
+ /**
+ * Handle mouse motion events.
+ *
+ * @param mouse mouse motion event
+ */
+ @Override
+ public void onMouseMotion(final TMouseEvent mouse) {
+ if (hideMouseWhenTyping) {
+ typingHidMouse = false;
+ }
+
+ if ((emulator != null) && (mouseOnEmulator(mouse))) {
+ emulator.addUserEvent(mouse);
+ readEmulatorState();
+ return;
+ }
+
+ // Emulator didn't consume it, pass it on
+ super.onMouseMotion(mouse);
+ }
+
+ // ------------------------------------------------------------------------
+ // TScrollableWidget ------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Draw the display buffer.
+ */
+ @Override
+ public void draw() {
+ if (emulator == null) {
+ return;
+ }
+
+ int width = getDisplayWidth();
+
+ boolean syncEmulator = false;
+ if ((System.currentTimeMillis() - lastUpdateTime >= 20)
+ && (dirty == true)
+ ) {
+ // Too much time has passed, draw it all.
+ syncEmulator = true;
+ } else if (emulator.isReading() && (dirty == false)) {
+ // Wait until the emulator has brought more data in.
+ syncEmulator = false;
+ } else if (!emulator.isReading() && (dirty == true)) {
+ // The emulator won't receive more data, update the display.
+ syncEmulator = true;
+ }
+
+ if ((syncEmulator == true)
+ || (display == null)
+ ) {
+ // We want to minimize the amount of time we have the emulator
+ // locked. Grab a copy of its display.
+ synchronized (emulator) {
+ // Update the scroll bars
+ reflowData();
+
+ if (!isDrawable()) {
+ // We lost the connection, onShellExit() called an action
+ // that ultimately removed this widget from the UI
+ // hierarchy, so no one cares if we update the display.
+ // Bail out.
+ return;
+ }
+
+ if ((display == null) || emulator.isReading()) {
+ display = emulator.getVisibleDisplay(getHeight(),
+ -getVerticalValue());
+ assert (display.size() == getHeight());
+ }
+ width = emulator.getWidth();
+ }
+ dirty = false;
+ }
+
+ // Now draw the emulator screen
+ int row = 0;
+ for (DisplayLine line: display) {
+ int widthMax = width;
+ if (line.isDoubleWidth()) {
+ widthMax /= 2;
+ }
+ if (widthMax > getWidth()) {
+ widthMax = getWidth();
+ }
+ for (int i = 0; i < widthMax; i++) {
+ Cell ch = line.charAt(i);
+
+ if (ch.isImage()) {
+ putCharXY(i, row, ch);
+ continue;
+ }
+
+ Cell newCell = new Cell(ch);
+ boolean reverse = line.isReverseColor() ^ ch.isReverse();
+ newCell.setReverse(false);
+ if (reverse) {
+ if (ch.getForeColorRGB() < 0) {
+ newCell.setBackColor(ch.getForeColor());
+ newCell.setBackColorRGB(-1);
+ } else {
+ newCell.setBackColorRGB(ch.getForeColorRGB());
+ }
+ if (ch.getBackColorRGB() < 0) {
+ newCell.setForeColor(ch.getBackColor());
+ newCell.setForeColorRGB(-1);
+ } else {
+ newCell.setForeColorRGB(ch.getBackColorRGB());
+ }
+ }
+ if (line.isDoubleWidth()) {
+ putDoubleWidthCharXY(line, (i * 2), row, newCell);
+ } else {
+ putCharXY(i, row, newCell);
+ }
+ }
+ row++;
+ }
+ }
+
+ /**
+ * Set current value of the vertical scroll.
+ *
+ * @param value the new scroll value
+ */
+ @Override
+ public void setVerticalValue(final int value) {
+ super.setVerticalValue(value);
+ dirty = true;
+ }
+
+ /**
+ * Perform a small step change up.
+ */
+ @Override
+ public void verticalDecrement() {
+ super.verticalDecrement();
+ dirty = true;
+ }
+
+ /**
+ * Perform a small step change down.
+ */
+ @Override
+ public void verticalIncrement() {
+ super.verticalIncrement();
+ dirty = true;
+ }
+
+ /**
+ * Perform a big step change up.
+ */
+ public void bigVerticalDecrement() {
+ super.bigVerticalDecrement();
+ dirty = true;
+ }
+
+ /**
+ * Perform a big step change down.
+ */
+ public void bigVerticalIncrement() {
+ super.bigVerticalIncrement();
+ dirty = true;
+ }
+
+ /**
+ * Go to the top edge of the vertical scroller.
+ */
+ public void toTop() {
+ super.toTop();
+ dirty = true;
+ }
+
+ /**
+ * Go to the bottom edge of the vertical scroller.
+ */
+ public void toBottom() {
+ super.toBottom();
+ dirty = true;
+ }
+
+ /**
+ * Handle widget close.
+ */
+ @Override
+ public void close() {
+ if (emulator != null) {
+ emulator.close();
+ }
+ if (shell != null) {
+ terminateShellChildProcess();
+ shell.destroy();
+ shell = null;
+ }
+ }
+
+ /**
+ * Resize scrollbars for a new width/height.
+ */
+ @Override
+ public void reflowData() {
+ if (emulator == null) {
+ return;
+ }
+
+ // Synchronize against the emulator so we don't stomp on its reader
+ // thread.
+ synchronized (emulator) {
+
+ // Pull cursor information
+ readEmulatorState();
+
+ // Vertical scrollbar
+ setTopValue(getHeight()
+ - (emulator.getScrollbackBuffer().size()
+ + emulator.getDisplayBuffer().size()));
+ setVerticalBigChange(getHeight());
+
+ } // synchronized (emulator)
+ }
+
+ // ------------------------------------------------------------------------
+ // TTerminalWidget --------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get the desired window title.
+ *
+ * @return the title
+ */
+ public String getTitle() {
+ return title;
+ }
+
+ /**
+ * Returns true if this widget does not want the application-wide mouse
+ * cursor drawn over it.
+ *
+ * @return true if this widget does not want the application-wide mouse
+ * cursor drawn over it
+ */
+ public boolean hasHiddenMouse() {
+ if (emulator == null) {
+ return false;
+ }
+ return (emulator.hasHiddenMousePointer() || typingHidMouse);
+ }
+
+ /**
+ * See if the terminal is still running.
+ *
+ * @return if true, we are still connected to / reading from the remote
+ * side
+ */
+ public boolean isReading() {
+ if (emulator == null) {
+ return false;
+ }
+ return emulator.isReading();
+ }
+
+ /**
+ * Convert a string array to a whitespace-separated string.
+ *
+ * @param array the string array
+ * @return a single string
+ */
+ private String stringArrayToString(final String [] array) {
+ StringBuilder sb = new StringBuilder(array[0].length());
+ for (int i = 0; i < array.length; i++) {
+ sb.append(array[i]);
+ if (i < array.length - 1) {
+ sb.append(' ');
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Spawn the shell.
+ *
+ * @param command the command line to execute
+ */
+ private void spawnShell(final String [] command) {
+
+ /*
+ System.err.printf("spawnShell(): '%s'\n",
+ stringArrayToString(command));
+ */
+
+ // We will have vScroller for its data fields and mouse event
+ // handling, but do not want to draw it.
+ vScroller = new TVScroller(null, getWidth(), 0, getHeight());
+ vScroller.setVisible(false);
+ setBottomValue(0);
+
+ title = i18n.getString("windowTitle");
+
+ // Assume XTERM
+ ECMA48.DeviceType deviceType = ECMA48.DeviceType.XTERM;
+
+ try {
+ ProcessBuilder pb = new ProcessBuilder(command);
+ Map<String, String> env = pb.environment();
+ env.put("TERM", ECMA48.deviceTypeTerm(deviceType));
+ env.put("LANG", ECMA48.deviceTypeLang(deviceType, "en"));
+ env.put("COLUMNS", "80");
+ env.put("LINES", "24");
+ pb.redirectErrorStream(true);
+ shell = pb.start();
+ emulator = new ECMA48(deviceType, shell.getInputStream(),
+ shell.getOutputStream(), this);
+ } catch (IOException e) {
+ messageBox(i18n.getString("errorLaunchingShellTitle"),
+ MessageFormat.format(i18n.getString("errorLaunchingShellText"),
+ e.getMessage()));
+ }
+
+ // Setup the scroll bars
+ onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
+ getHeight()));
+
+ // Hide mouse when typing option
+ if (System.getProperty("jexer.TTerminal.hideMouseWhenTyping",
+ "true").equals("false")) {
+
+ hideMouseWhenTyping = false;
+ }
+ }
+
+ /**
+ * Terminate the child of the 'script' process used on POSIX. This may
+ * or may not work.
+ */
+ private void terminateShellChildProcess() {
+ int pid = -1;
+ if (shell.getClass().getName().equals("java.lang.UNIXProcess")) {
+ /* get the PID on unix/linux systems */
+ try {
+ Field field = shell.getClass().getDeclaredField("pid");
+ field.setAccessible(true);
+ pid = field.getInt(shell);
+ } catch (Throwable e) {
+ // SQUASH, this didn't work. Just bail out quietly.
+ return;
+ }
+ }
+ if (pid != -1) {
+ // shell.destroy() works successfully at killing this side of
+ // 'script'. But we need to make sure the other side (child
+ // process) is also killed.
+ String [] cmdKillIt = {
+ "pkill", "-P", Integer.toString(pid)
+ };
+ try {
+ Runtime.getRuntime().exec(cmdKillIt);
+ } catch (Throwable e) {
+ // SQUASH, this didn't work. Just bail out quietly.
+ return;
+ }
+ }
+ }
+
+ /**
+ * Hook for subclasses to be notified of the shell termination.
+ */
+ public void onShellExit() {
+ TApplication app = getApplication();
+ if (app != null) {
+ if (closeAction != null) {
+ // We have to put this action inside invokeLater() because it
+ // could be executed during draw() when syncing with ECMA48.
+ app.invokeLater(new Runnable() {
+ public void run() {
+ closeAction.DO(TTerminalWidget.this);
+ }
+ });
+ }
+ if (getApplication() != null) {
+ getApplication().postEvent(new TMenuEvent(
+ TMenu.MID_REPAINT));
+ }
+ }
+ }
+
+ /**
+ * Copy out variables from the emulator that TTerminal has to expose on
+ * screen.
+ */
+ private void readEmulatorState() {
+ if (emulator == null) {
+ return;
+ }
+
+ // Synchronize against the emulator so we don't stomp on its reader
+ // thread.
+ synchronized (emulator) {
+
+ setCursorX(emulator.getCursorX());
+ setCursorY(emulator.getCursorY()
+ + (getHeight() - emulator.getHeight())
+ - getVerticalValue());
+ setCursorVisible(emulator.isCursorVisible());
+ if (getCursorX() > getWidth()) {
+ setCursorVisible(false);
+ }
+ if ((getCursorY() >= getHeight()) || (getCursorY() < 0)) {
+ setCursorVisible(false);
+ }
+ if (emulator.getScreenTitle().length() > 0) {
+ // Only update the title if the shell is still alive
+ if (shell != null) {
+ title = emulator.getScreenTitle();
+ }
+ }
+
+ // Check to see if the shell has died.
+ if (!emulator.isReading() && (shell != null)) {
+ try {
+ int rc = shell.exitValue();
+ // The emulator exited on its own, all is fine
+ title = MessageFormat.format(i18n.
+ getString("windowTitleCompleted"), title, rc);
+ exitValue = rc;
+ shell = null;
+ emulator.close();
+ onShellExit();
+ } catch (IllegalThreadStateException e) {
+ // The emulator thread has exited, but the shell Process
+ // hasn't figured that out yet. Do nothing, we will see
+ // this in a future tick.
+ }
+ } else if (emulator.isReading() && (shell != null)) {
+ // The shell might be dead, let's check
+ try {
+ int rc = shell.exitValue();
+ // If we got here, the shell died.
+ title = MessageFormat.format(i18n.
+ getString("windowTitleCompleted"), title, rc);
+ exitValue = rc;
+ shell = null;
+ emulator.close();
+ onShellExit();
+ } catch (IllegalThreadStateException e) {
+ // The shell is still running, do nothing.
+ }
+ }
+
+ } // synchronized (emulator)
+ }
+
+ /**
+ * Check if a mouse press/release/motion event coordinate is over the
+ * emulator.
+ *
+ * @param mouse a mouse-based event
+ * @return whether or not the mouse is on the emulator
+ */
+ private boolean mouseOnEmulator(final TMouseEvent mouse) {
+ if (emulator == null) {
+ return false;
+ }
+
+ if (!emulator.isReading()) {
+ return false;
+ }
+
+ if ((mouse.getX() >= 0)
+ && (mouse.getX() < getWidth() - 1)
+ && (mouse.getY() >= 0)
+ && (mouse.getY() < getHeight())
+ ) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Draw glyphs for a double-width or double-height VT100 cell to two
+ * screen cells.
+ *
+ * @param line the line this VT100 cell is in
+ * @param x the X position to draw the left half to
+ * @param y the Y position to draw to
+ * @param cell the cell to draw
+ */
+ private void putDoubleWidthCharXY(final DisplayLine line, final int x,
+ final int y, final Cell cell) {
+
+ int textWidth = getScreen().getTextWidth();
+ int textHeight = getScreen().getTextHeight();
+ boolean cursorBlinkVisible = true;
+
+ if (getScreen() instanceof SwingTerminal) {
+ SwingTerminal terminal = (SwingTerminal) getScreen();
+ cursorBlinkVisible = terminal.getCursorBlinkVisible();
+ } else if (getScreen() instanceof ECMA48Terminal) {
+ ECMA48Terminal terminal = (ECMA48Terminal) getScreen();
+
+ if (!terminal.hasSixel()) {
+ // The backend does not have sixel support, draw this as text
+ // and bail out.
+ putCharXY(x, y, cell);
+ putCharXY(x + 1, y, ' ', cell);
+ return;
+ }
+ cursorBlinkVisible = blinkState;
+ } else {
+ // We don't know how to dray glyphs to this screen, draw them as
+ // text and bail out.
+ putCharXY(x, y, cell);
+ putCharXY(x + 1, y, ' ', cell);
+ return;
+ }
+
+ if ((textWidth != lastTextWidth) || (textHeight != lastTextHeight)) {
+ // Screen size has changed, reset the font.
+ setupFont(textHeight);
+ lastTextWidth = textWidth;
+ lastTextHeight = textHeight;
+ }
+ assert (doubleFont != null);
+
+ BufferedImage image;
+ if (line.getDoubleHeight() == 1) {
+ // Double-height top half: don't draw the underline.
+ Cell newCell = new Cell(cell);
+ newCell.setUnderline(false);
+ image = doubleFont.getImage(newCell, textWidth * 2, textHeight * 2,
+ cursorBlinkVisible);
+ } else {
+ image = doubleFont.getImage(cell, textWidth * 2, textHeight * 2,
+ cursorBlinkVisible);
+ }
+
+ // Now that we have the double-wide glyph drawn, copy the right
+ // pieces of it to the cells.
+ Cell left = new Cell(cell);
+ Cell right = new Cell(cell);
+ right.setChar(' ');
+ BufferedImage leftImage = null;
+ BufferedImage rightImage = null;
+ /*
+ System.err.println("image " + image + " textWidth " + textWidth +
+ " textHeight " + textHeight);
+ */
+
+ switch (line.getDoubleHeight()) {
+ case 1:
+ // Top half double height
+ leftImage = image.getSubimage(0, 0, textWidth, textHeight);
+ rightImage = image.getSubimage(textWidth, 0, textWidth, textHeight);
+ break;
+ case 2:
+ // Bottom half double height
+ leftImage = image.getSubimage(0, textHeight, textWidth, textHeight);
+ rightImage = image.getSubimage(textWidth, textHeight,
+ textWidth, textHeight);
+ break;
+ default:
+ // Either single height double-width, or error fallback
+ BufferedImage wideImage = new BufferedImage(textWidth * 2,
+ textHeight, BufferedImage.TYPE_INT_ARGB);
+ Graphics2D grWide = wideImage.createGraphics();
+ grWide.drawImage(image, 0, 0, wideImage.getWidth(),
+ wideImage.getHeight(), null);
+ grWide.dispose();
+ leftImage = wideImage.getSubimage(0, 0, textWidth, textHeight);
+ rightImage = wideImage.getSubimage(textWidth, 0, textWidth,
+ textHeight);
+ break;
+ }
+ left.setImage(leftImage);
+ right.setImage(rightImage);
+ // Since we have image data, ditch the character here. Otherwise, a
+ // drawBoxShadow() over the terminal window will show the characters
+ // which looks wrong.
+ left.setChar(' ');
+ right.setChar(' ');
+ putCharXY(x, y, left);
+ putCharXY(x + 1, y, right);
+ }
+
+ /**
+ * Set up the double-width font.
+ *
+ * @param fontSize the size of font to request for the single-width font.
+ * The double-width font will be 2x this value.
+ */
+ private void setupFont(final int fontSize) {
+ doubleFont = GlyphMaker.getInstance(fontSize * 2);
+
+ // Special case: the ECMA48 backend needs to have a timer to drive
+ // its blink state.
+ if (getScreen() instanceof jexer.backend.ECMA48Terminal) {
+ if (!haveTimer) {
+ // Blink every 500 millis.
+ long millis = 500;
+ getApplication().addTimer(millis, true,
+ new TAction() {
+ public void DO() {
+ blinkState = !blinkState;
+ getApplication().doRepaint();
+ }
+ }
+ );
+ haveTimer = true;
+ }
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // DisplayListener --------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Called by emulator when fresh data has come in.
+ */
+ public void displayChanged() {
+ dirty = true;
+ getApplication().postEvent(new TMenuEvent(TMenu.MID_REPAINT));
+ }
+
+ /**
+ * Function to call to obtain the display width.
+ *
+ * @return the number of columns in the display
+ */
+ public int getDisplayWidth() {
+ if (ptypipe) {
+ return getWidth();
+ }
+ return 80;
+ }
+
+ /**
+ * Function to call to obtain the display height.
+ *
+ * @return the number of rows in the display
+ */
+ public int getDisplayHeight() {
+ if (ptypipe) {
+ return getHeight();
+ }
+ return 24;
+ }
+
+ }
--- /dev/null
+ windowTitle=Terminal
+ errorLaunchingShellTitle=Error
+ errorLaunchingShellText=Error launching shell: {0}
+ statusBarRunning=Terminal session executing...
+ windowTitleCompleted={0} [Completed - {1}]
+ statusBarCompleted=Terminal session completed, exit code {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;
+
+ 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.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.TKeypress.*;
+
+ /**
+ * TTerminalWindow exposes a ECMA-48 / ANSI X3.64 style terminal in a window.
+ */
+ public class TTerminalWindow extends TScrollableWindow {
+
+ /**
+ * Translated strings.
+ */
+ private static final ResourceBundle i18n = ResourceBundle.getBundle(TTerminalWindow.class.getName());
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The terminal.
+ */
+ private TTerminalWidget terminal;
+
+ /**
+ * If true, close the window when the shell exits.
+ */
+ private boolean closeOnExit = false;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor spawns a custom command line.
+ *
+ * @param application TApplication that manages this window
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param commandLine the command line to execute
+ */
+ public TTerminalWindow(final TApplication application, final int x,
+ final int y, final String commandLine) {
+
+ this(application, x, y, RESIZABLE, commandLine.split("\\s+"),
+ System.getProperty("jexer.TTerminal.closeOnExit",
+ "false").equals("true"));
+ }
+
+ /**
+ * Public constructor spawns a custom command line.
+ *
+ * @param application TApplication that manages this window
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param commandLine the command line to execute
+ * @param closeOnExit if true, close the window when the command exits
+ */
+ public TTerminalWindow(final TApplication application, final int x,
+ final int y, final String commandLine, final boolean closeOnExit) {
+
+ this(application, x, y, RESIZABLE, commandLine.split("\\s+"),
+ closeOnExit);
+ }
+
+ /**
+ * Public constructor spawns a custom command line.
+ *
+ * @param application TApplication that manages this window
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param flags mask of CENTERED, MODAL, or RESIZABLE
+ * @param command the command line to execute
+ */
+ public TTerminalWindow(final TApplication application, final int x,
+ final int y, final int flags, final String [] command) {
+
+ this(application, x, y, flags, command,
+ System.getProperty("jexer.TTerminal.closeOnExit",
+ "false").equals("true"));
+ }
+
+ /**
+ * Public constructor spawns a custom command line.
+ *
+ * @param application TApplication that manages this window
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param flags mask of CENTERED, MODAL, or RESIZABLE
+ * @param command the command line to execute
+ * @param closeOnExit if true, close the window when the command exits
+ */
+ public TTerminalWindow(final TApplication application, final int x,
+ final int y, final int flags, final String [] command,
+ final boolean closeOnExit) {
+
+ super(application, i18n.getString("windowTitle"), x, y,
+ 80 + 2, 24 + 2, flags);
+
+ // Require at least one line for the display.
+ setMinimumWindowHeight(3);
+
+ this.closeOnExit = closeOnExit;
+ vScroller = new TVScroller(this, getWidth() - 2, 0, getHeight() - 2);
+
+ // Claim the keystrokes the emulator will need.
+ addShortcutKeys();
+
+ // Add shortcut text
+ newStatusBar(i18n.getString("statusBarRunning"));
+
+ // Spin it up
+ terminal = new TTerminalWidget(this, 0, 0, new TAction() {
+ public void DO() {
+ onShellExit();
+ }
+ });
+ }
+
+ /**
+ * Public constructor spawns a shell.
+ *
+ * @param application TApplication that manages this window
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param flags mask of CENTERED, MODAL, or RESIZABLE
+ */
+ public TTerminalWindow(final TApplication application, final int x,
+ final int y, final int flags) {
+
+ this(application, x, y, flags,
+ System.getProperty("jexer.TTerminal.closeOnExit",
+ "false").equals("true"));
+
+ }
+
+ /**
+ * Public constructor spawns a shell.
+ *
+ * @param application TApplication that manages this window
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param flags mask of CENTERED, MODAL, or RESIZABLE
+ * @param closeOnExit if true, close the window when the shell exits
+ */
+ public TTerminalWindow(final TApplication application, final int x,
+ final int y, final int flags, final boolean closeOnExit) {
+
+ super(application, i18n.getString("windowTitle"), x, y,
+ 80 + 2, 24 + 2, flags);
+
+ // Require at least one line for the display.
+ setMinimumWindowHeight(3);
+
+ this.closeOnExit = closeOnExit;
+ vScroller = new TVScroller(this, getWidth() - 2, 0, getHeight() - 2);
+
+ // Claim the keystrokes the emulator will need.
+ addShortcutKeys();
+
+ // Add shortcut text
+ newStatusBar(i18n.getString("statusBarRunning"));
+
+ // Spin it up
+ terminal = new TTerminalWidget(this, 0, 0, new TAction() {
+ public void DO() {
+ onShellExit();
+ }
+ });
+ }
+
+ // ------------------------------------------------------------------------
+ // TScrollableWindow ------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Draw the display buffer.
+ */
+ @Override
+ public void draw() {
+ if (terminal != null) {
+ setTitle(terminal.getTitle());
+ }
+ reflowData();
+ super.draw();
+ }
+
+ /**
+ * Handle window/screen resize events.
+ *
+ * @param resize resize event
+ */
+ @Override
+ public void onResize(final TResizeEvent resize) {
+ if (resize.getType() == TResizeEvent.Type.WIDGET) {
+ if (terminal != null) {
+ terminal.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
+ getWidth() - 2, getHeight() - 2));
+ }
+
+ // Resize the scroll bars
+ reflowData();
+ placeScrollbars();
+ }
+ return;
+ }
+
+ /**
+ * Resize scrollbars for a new width/height.
+ */
+ @Override
+ public void reflowData() {
+ // Vertical scrollbar
+ if (terminal != null) {
+ terminal.reflowData();
+ setTopValue(terminal.getTopValue());
+ setBottomValue(terminal.getBottomValue());
+ setVerticalBigChange(terminal.getVerticalBigChange());
+ setVerticalValue(terminal.getVerticalValue());
+ }
+ }
+
+ /**
+ * Handle keystrokes.
+ *
+ * @param keypress keystroke event
+ */
+ @Override
+ public void onKeypress(final TKeypressEvent keypress) {
+ if ((terminal != null) && (terminal.isReading())) {
+ terminal.onKeypress(keypress);
+ } else {
+ super.onKeypress(keypress);
+ }
+ }
+
+ /**
+ * Handle mouse press events.
+ *
+ * @param mouse mouse button press event
+ */
+ @Override
+ public void onMouseDown(final TMouseEvent mouse) {
+ if (inWindowMove || inWindowResize) {
+ // TWindow needs to deal with this.
+ super.onMouseDown(mouse);
+ return;
+ }
+
+ super.onMouseDown(mouse);
+ }
+
+ /**
+ * Handle mouse release events.
+ *
+ * @param mouse mouse button release event
+ */
+ @Override
+ public void onMouseUp(final TMouseEvent mouse) {
+ if (inWindowMove || inWindowResize) {
+ // TWindow needs to deal with this.
+ super.onMouseUp(mouse);
+ return;
+ }
+
+ super.onMouseUp(mouse);
+
+ if (mouse.isMouse1() && mouseOnVerticalScroller(mouse)) {
+ // Clicked on vertical scrollbar
+ if (terminal != null) {
+ terminal.setVerticalValue(getVerticalValue());
+ }
+ }
+ }
+
+ /**
+ * Handle mouse motion events.
+ *
+ * @param mouse mouse motion event
+ */
+ @Override
+ public void onMouseMotion(final TMouseEvent mouse) {
+ if (inWindowMove || inWindowResize) {
+ // TWindow needs to deal with this.
+ super.onMouseMotion(mouse);
+ return;
+ }
+
+ super.onMouseMotion(mouse);
+
+ if (mouse.isMouse1() && mouseOnVerticalScroller(mouse)) {
+ // Clicked/dragged on vertical scrollbar
+ if (terminal != null) {
+ terminal.setVerticalValue(getVerticalValue());
+ }
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // TTerminalWindow --------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Returns true if this window does not want the application-wide mouse
+ * cursor drawn over it.
+ *
+ * @return true if this window does not want the application-wide mouse
+ * cursor drawn over it
+ */
+ @Override
+ public boolean hasHiddenMouse() {
+ if (terminal != null) {
+ return terminal.hasHiddenMouse();
+ }
+ return false;
+ }
+
+ /**
+ * Claim the keystrokes the emulator will need.
+ */
+ private void addShortcutKeys() {
+ addShortcutKeypress(kbCtrlA);
+ addShortcutKeypress(kbCtrlB);
+ addShortcutKeypress(kbCtrlC);
+ addShortcutKeypress(kbCtrlD);
+ addShortcutKeypress(kbCtrlE);
+ addShortcutKeypress(kbCtrlF);
+ addShortcutKeypress(kbCtrlG);
+ addShortcutKeypress(kbCtrlH);
+ addShortcutKeypress(kbCtrlU);
+ addShortcutKeypress(kbCtrlJ);
+ addShortcutKeypress(kbCtrlK);
+ addShortcutKeypress(kbCtrlL);
+ addShortcutKeypress(kbCtrlM);
+ addShortcutKeypress(kbCtrlN);
+ addShortcutKeypress(kbCtrlO);
+ addShortcutKeypress(kbCtrlP);
+ addShortcutKeypress(kbCtrlQ);
+ addShortcutKeypress(kbCtrlR);
+ addShortcutKeypress(kbCtrlS);
+ addShortcutKeypress(kbCtrlT);
+ addShortcutKeypress(kbCtrlU);
+ addShortcutKeypress(kbCtrlV);
+ addShortcutKeypress(kbCtrlW);
+ addShortcutKeypress(kbCtrlX);
+ addShortcutKeypress(kbCtrlY);
+ addShortcutKeypress(kbCtrlZ);
+ addShortcutKeypress(kbF1);
+ addShortcutKeypress(kbF2);
+ addShortcutKeypress(kbF3);
+ addShortcutKeypress(kbF4);
+ addShortcutKeypress(kbF5);
+ addShortcutKeypress(kbF6);
+ addShortcutKeypress(kbF7);
+ addShortcutKeypress(kbF8);
+ addShortcutKeypress(kbF9);
+ addShortcutKeypress(kbF10);
+ addShortcutKeypress(kbF11);
+ addShortcutKeypress(kbF12);
+ addShortcutKeypress(kbAltA);
+ addShortcutKeypress(kbAltB);
+ addShortcutKeypress(kbAltC);
+ addShortcutKeypress(kbAltD);
+ addShortcutKeypress(kbAltE);
+ addShortcutKeypress(kbAltF);
+ addShortcutKeypress(kbAltG);
+ addShortcutKeypress(kbAltH);
+ addShortcutKeypress(kbAltU);
+ addShortcutKeypress(kbAltJ);
+ addShortcutKeypress(kbAltK);
+ addShortcutKeypress(kbAltL);
+ addShortcutKeypress(kbAltM);
+ addShortcutKeypress(kbAltN);
+ addShortcutKeypress(kbAltO);
+ addShortcutKeypress(kbAltP);
+ addShortcutKeypress(kbAltQ);
+ addShortcutKeypress(kbAltR);
+ addShortcutKeypress(kbAltS);
+ addShortcutKeypress(kbAltT);
+ addShortcutKeypress(kbAltU);
+ addShortcutKeypress(kbAltV);
+ addShortcutKeypress(kbAltW);
+ addShortcutKeypress(kbAltX);
+ addShortcutKeypress(kbAltY);
+ addShortcutKeypress(kbAltZ);
+ }
+
+ /**
+ * Hook for subclasses to be notified of the shell termination.
+ */
+ public void onShellExit() {
+ if (closeOnExit) {
+ close();
+ }
+ clearShortcutKeypresses();
+ getApplication().postEvent(new TMenuEvent(TMenu.MID_REPAINT));
+ }
+
+ }
--- /dev/null
+ windowTitle=Terminal
+ statusBarRunning=Terminal session executing...
--- /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.Arrays;
+ import java.util.LinkedList;
+ import java.util.List;
+
+ import jexer.bits.CellAttributes;
+ import jexer.bits.StringUtils;
+ import jexer.event.TKeypressEvent;
+ import jexer.event.TMouseEvent;
+ import static jexer.TKeypress.kbDown;
+ import static jexer.TKeypress.kbEnd;
+ import static jexer.TKeypress.kbHome;
+ import static jexer.TKeypress.kbLeft;
+ import static jexer.TKeypress.kbPgDn;
+ import static jexer.TKeypress.kbPgUp;
+ import static jexer.TKeypress.kbRight;
+ import static jexer.TKeypress.kbUp;
+
+ /**
+ * TText implements a simple scrollable text area. It reflows automatically on
+ * resize.
+ */
+ public class TText extends TScrollableWidget {
+
+ // ------------------------------------------------------------------------
+ // Constants --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Available text justifications.
+ */
+ public enum Justification {
+
+ /**
+ * Not justified at all, use spacing as provided by the client.
+ */
+ NONE,
+
+ /**
+ * Left-justified text.
+ */
+ LEFT,
+
+ /**
+ * Centered text.
+ */
+ CENTER,
+
+ /**
+ * Right-justified text.
+ */
+ RIGHT,
+
+ /**
+ * Fully-justified text.
+ */
+ FULL,
+ }
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * How to justify the text.
+ */
+ private Justification justification = Justification.LEFT;
+
+ /**
+ * Text to display.
+ */
+ private String text;
+
+ /**
+ * Text converted to lines.
+ */
+ private List<String> lines;
+
+ /**
+ * Text color.
+ */
+ private String colorKey;
+
+ /**
+ * Maximum width of a single line.
+ */
+ private int maxLineWidth;
+
+ /**
+ * Number of lines between each paragraph.
+ */
+ private int lineSpacing = 1;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param text text on the screen
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of text area
+ * @param height height of text area
+ */
+ public TText(final TWidget parent, final String text, final int x,
+ final int y, final int width, final int height) {
+
+ this(parent, text, x, y, width, height, "ttext");
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param text text on the screen
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of text area
+ * @param height height of text area
+ * @param colorKey ColorTheme key color to use for foreground
+ * text. Default is "ttext".
+ */
+ public TText(final TWidget parent, final String text, final int x,
+ final int y, final int width, final int height,
+ final String colorKey) {
+
+ // Set parent and window
+ super(parent, x, y, width, height);
+
+ this.text = text;
+ this.colorKey = colorKey;
+
+ lines = new LinkedList<String>();
+
+ vScroller = new TVScroller(this, getWidth() - 1, 0,
+ Math.max(1, getHeight() - 1));
+ hScroller = new THScroller(this, 0, getHeight() - 1,
+ Math.max(1, getWidth() - 1));
+ reflowData();
+ }
+
+ // ------------------------------------------------------------------------
+ // 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(getHeight() - 1);
+ }
+ }
+
+ /**
+ * Draw the text box.
+ */
+ @Override
+ public void draw() {
+ // Setup my color
+ CellAttributes color = getTheme().getColor(colorKey);
+
+ int begin = vScroller.getValue();
+ int topY = 0;
+ for (int i = begin; i < lines.size(); i++) {
+ String line = lines.get(i);
+ if (hScroller.getValue() < StringUtils.width(line)) {
+ line = line.substring(hScroller.getValue());
+ } else {
+ line = "";
+ }
+ if (getWidth() > 3) {
+ String formatString = "%-" + Integer.toString(getWidth() - 1) + "s";
+ putStringXY(0, topY, String.format(formatString, line), color);
+ }
+ topY++;
+
+ if (topY >= (getHeight() - 1)) {
+ break;
+ }
+ }
+
+ // Pad the rest with blank lines
+ for (int i = topY; i < (getHeight() - 1); i++) {
+ hLineXY(0, i, getWidth() - 1, ' ', color);
+ }
+
+ }
+
+ /**
+ * Handle mouse press events.
+ *
+ * @param mouse mouse button press event
+ */
+ @Override
+ public void onMouseDown(final TMouseEvent mouse) {
+ if (mouse.isMouseWheelUp()) {
+ vScroller.decrement();
+ return;
+ }
+ if (mouse.isMouseWheelDown()) {
+ vScroller.increment();
+ return;
+ }
+
+ // Pass to children
+ super.onMouseDown(mouse);
+ }
+
+ /**
+ * Handle keystrokes.
+ *
+ * @param keypress keystroke event
+ */
+ @Override
+ public void onKeypress(final TKeypressEvent keypress) {
+ if (keypress.equals(kbLeft)) {
+ hScroller.decrement();
+ } else if (keypress.equals(kbRight)) {
+ hScroller.increment();
+ } else if (keypress.equals(kbUp)) {
+ vScroller.decrement();
+ } else if (keypress.equals(kbDown)) {
+ vScroller.increment();
+ } else if (keypress.equals(kbPgUp)) {
+ vScroller.bigDecrement();
+ } else if (keypress.equals(kbPgDn)) {
+ vScroller.bigIncrement();
+ } else if (keypress.equals(kbHome)) {
+ vScroller.toTop();
+ } else if (keypress.equals(kbEnd)) {
+ vScroller.toBottom();
+ } else {
+ // Pass other keys (tab etc.) on
+ super.onKeypress(keypress);
+ }
+ }
+
+ /**
+ * Resize text and scrollbars for a new width/height.
+ */
+ @Override
+ public void reflowData() {
+ // Reset the lines
+ lines.clear();
+
+ // Break up text into paragraphs
+ String[] paragraphs = text.split("\n\n");
+ for (String p : paragraphs) {
+ switch (justification) {
+ case NONE:
+ lines.addAll(Arrays.asList(p.split("\n")));
+ break;
+ case LEFT:
+ lines.addAll(jexer.bits.StringUtils.left(p,
+ getWidth() - 1));
+ break;
+ case CENTER:
+ lines.addAll(jexer.bits.StringUtils.center(p,
+ getWidth() - 1));
+ break;
+ case RIGHT:
+ lines.addAll(jexer.bits.StringUtils.right(p,
+ getWidth() - 1));
+ break;
+ case FULL:
+ lines.addAll(jexer.bits.StringUtils.full(p,
+ getWidth() - 1));
+ break;
+ }
+
+ for (int i = 0; i < lineSpacing; i++) {
+ lines.add("");
+ }
+ }
+ computeBounds();
+ }
+
+ // ------------------------------------------------------------------------
+ // TText ------------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Set the text.
+ *
+ * @param text new text to display
+ */
+ public void setText(final String text) {
+ this.text = text;
+ reflowData();
+ }
+
+ /**
+ * Get the text.
+ *
+ * @return the text
+ */
+ public String getText() {
+ return text;
+ }
+
+ /**
+ * Convenience method used by TWindowLoggerOutput.
+ *
+ * @param line new line to add
+ */
+ public void addLine(final String line) {
+ if (StringUtils.width(text) == 0) {
+ text = line;
+ } else {
+ text += "\n\n";
+ text += line;
+ }
+ reflowData();
+ }
+
+ /**
+ * Recompute the bounds for the scrollbars.
+ */
+ private void computeBounds() {
+ maxLineWidth = 0;
+ for (String line : lines) {
+ if (StringUtils.width(line) > maxLineWidth) {
+ maxLineWidth = StringUtils.width(line);
+ }
+ }
+
+ vScroller.setTopValue(0);
+ vScroller.setBottomValue((lines.size() - getHeight()) + 1);
+ if (vScroller.getBottomValue() < 0) {
+ vScroller.setBottomValue(0);
+ }
+ if (vScroller.getValue() > vScroller.getBottomValue()) {
+ vScroller.setValue(vScroller.getBottomValue());
+ }
+
+ hScroller.setLeftValue(0);
+ hScroller.setRightValue((maxLineWidth - getWidth()) + 1);
+ if (hScroller.getRightValue() < 0) {
+ hScroller.setRightValue(0);
+ }
+ if (hScroller.getValue() > hScroller.getRightValue()) {
+ hScroller.setValue(hScroller.getRightValue());
+ }
+ }
+
+ /**
+ * Set justification.
+ *
+ * @param justification LEFT, CENTER, RIGHT, or FULL
+ */
+ public void setJustification(final Justification justification) {
+ this.justification = justification;
+ reflowData();
+ }
+
+ /**
+ * Left-justify the text.
+ */
+ public void leftJustify() {
+ justification = Justification.LEFT;
+ reflowData();
+ }
+
+ /**
+ * Center-justify the text.
+ */
+ public void centerJustify() {
+ justification = Justification.CENTER;
+ reflowData();
+ }
+
+ /**
+ * Right-justify the text.
+ */
+ public void rightJustify() {
+ justification = Justification.RIGHT;
+ reflowData();
+ }
+
+ /**
+ * Fully-justify the text.
+ */
+ public void fullJustify() {
+ justification = Justification.FULL;
+ 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;
+
+ import java.util.Date;
+
+ /**
+ * TTimer implements a simple timer.
+ */
+ public class TTimer {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * If true, re-schedule after every tick. Note package private access.
+ */
+ boolean recurring = false;
+
+ /**
+ * Duration (in millis) between ticks if this is a recurring timer.
+ */
+ private long duration = 0;
+
+ /**
+ * The next time this timer needs to be ticked.
+ */
+ private Date nextTick;
+
+ /**
+ * The action to perfom on a tick.
+ */
+ private TAction action;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Package private constructor.
+ *
+ * @param duration number of milliseconds to wait between ticks
+ * @param recurring if true, re-schedule this timer after every tick
+ * @param action to perform on next tick
+ */
+ TTimer(final long duration, final boolean recurring, final TAction action) {
+
+ this.recurring = recurring;
+ this.duration = duration;
+ this.action = action;
+
+ Date now = new Date();
+ nextTick = new Date(now.getTime() + duration);
+ }
+
+ // ------------------------------------------------------------------------
+ // TTimer -----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get the next time this timer needs to be ticked. Note package private
+ * access.
+ *
+ * @return time at which action should be called
+ */
+ Date getNextTick() {
+ return nextTick;
+ }
+
+ /**
+ * Set the recurring flag.
+ *
+ * @param recurring if true, re-schedule this timer after every tick
+ */
+ public void setRecurring(final boolean recurring) {
+ this.recurring = recurring;
+ }
+
+ /**
+ * Tick this timer. Note package private access.
+ */
+ void tick() {
+ if (action != null) {
+ action.DO();
+ }
+ // Set next tick
+ Date ticked = new Date();
+ if (recurring) {
+ nextTick = new Date(ticked.getTime() + duration);
+ }
+ }
+
+ }
--- /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 jexer.bits.CellAttributes;
+ import jexer.bits.GraphicsChars;
+ import jexer.event.TMouseEvent;
+
+ /**
+ * TVScroller implements a simple vertical scroll bar.
+ */
+ public class TVScroller extends TWidget {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Value that corresponds to being on the top edge of the scroll bar.
+ */
+ private int topValue = 0;
+
+ /**
+ * Value that corresponds to being on the bottom edge of the scroll bar.
+ */
+ private int bottomValue = 100;
+
+ /**
+ * Current value of the scroll.
+ */
+ private int value = 0;
+
+ /**
+ * The increment for clicking on an arrow.
+ */
+ private int smallChange = 1;
+
+ /**
+ * The increment for clicking in the bar between the box and an arrow.
+ */
+ private int bigChange = 20;
+
+ /**
+ * When true, the user is dragging the scroll box.
+ */
+ private boolean inScroll = false;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param height height of scroll bar
+ */
+ public TVScroller(final TWidget parent, final int x, final int y,
+ final int height) {
+
+ // Set parent and window
+ super(parent, x, y, 1, height);
+ }
+
+ // ------------------------------------------------------------------------
+ // Event handlers ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Handle mouse button releases.
+ *
+ * @param mouse mouse button release event
+ */
+ @Override
+ public void onMouseUp(final TMouseEvent mouse) {
+ if (bottomValue == topValue) {
+ return;
+ }
+
+ if (inScroll) {
+ inScroll = false;
+ return;
+ }
+
+ if ((mouse.getX() == 0)
+ && (mouse.getY() == 0)
+ ) {
+ // Clicked on the top arrow
+ decrement();
+ return;
+ }
+
+ if ((mouse.getX() == 0)
+ && (mouse.getY() == getHeight() - 1)
+ ) {
+ // Clicked on the bottom arrow
+ increment();
+ return;
+ }
+
+ if ((mouse.getX() == 0)
+ && (mouse.getY() > 0)
+ && (mouse.getY() < boxPosition())
+ ) {
+ // Clicked between the top arrow and the box
+ value -= bigChange;
+ if (value < topValue) {
+ value = topValue;
+ }
+ return;
+ }
+
+ if ((mouse.getX() == 0)
+ && (mouse.getY() > boxPosition())
+ && (mouse.getY() < getHeight() - 1)
+ ) {
+ // Clicked between the box and the bottom arrow
+ value += bigChange;
+ if (value > bottomValue) {
+ value = bottomValue;
+ }
+ return;
+ }
+ }
+
+ /**
+ * Handle mouse movement events.
+ *
+ * @param mouse mouse motion event
+ */
+ @Override
+ public void onMouseMotion(final TMouseEvent mouse) {
+ if (bottomValue == topValue) {
+ return;
+ }
+
+ if ((mouse.isMouse1())
+ && (inScroll)
+ && (mouse.getY() > 0)
+ && (mouse.getY() < getHeight() - 1)
+ ) {
+ // Recompute value based on new box position
+ value = (bottomValue - topValue)
+ * (mouse.getY()) / (getHeight() - 3) + topValue;
+ if (value > bottomValue) {
+ value = bottomValue;
+ }
+ if (value < topValue) {
+ value = topValue;
+ }
+ return;
+ }
+
+ inScroll = false;
+ }
+
+ /**
+ * Handle mouse press events.
+ *
+ * @param mouse mouse button press event
+ */
+ @Override
+ public void onMouseDown(final TMouseEvent mouse) {
+ if (bottomValue == topValue) {
+ return;
+ }
+
+ if ((mouse.getX() == 0)
+ && (mouse.getY() == boxPosition())
+ ) {
+ inScroll = true;
+ return;
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // TWidget ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Draw a vertical scroll bar.
+ */
+ @Override
+ public void draw() {
+ CellAttributes arrowColor = getTheme().getColor("tscroller.arrows");
+ CellAttributes barColor = getTheme().getColor("tscroller.bar");
+ putCharXY(0, 0, GraphicsChars.CP437[0x1E], arrowColor);
+ putCharXY(0, getHeight() - 1, GraphicsChars.CP437[0x1F], arrowColor);
+
+ // Place the box
+ if (bottomValue > topValue) {
+ vLineXY(0, 1, getHeight() - 2, GraphicsChars.CP437[0xB1], barColor);
+ putCharXY(0, boxPosition(), GraphicsChars.BOX, arrowColor);
+ } else {
+ vLineXY(0, 1, getHeight() - 2, GraphicsChars.HATCH, barColor);
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // TVScroller -------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get the value that corresponds to being on the top edge of the scroll
+ * bar.
+ *
+ * @return the scroll value
+ */
+ public int getTopValue() {
+ return topValue;
+ }
+
+ /**
+ * Set the value that corresponds to being on the top edge of the scroll
+ * bar.
+ *
+ * @param topValue the new scroll value
+ */
+ public void setTopValue(final int topValue) {
+ this.topValue = topValue;
+ }
+
+ /**
+ * Get the value that corresponds to being on the bottom edge of the
+ * scroll bar.
+ *
+ * @return the scroll value
+ */
+ public int getBottomValue() {
+ return bottomValue;
+ }
+
+ /**
+ * Set the value that corresponds to being on the bottom edge of the
+ * scroll bar.
+ *
+ * @param bottomValue the new scroll value
+ */
+ public void setBottomValue(final int bottomValue) {
+ this.bottomValue = bottomValue;
+ }
+
+ /**
+ * Get current value of the scroll.
+ *
+ * @return the scroll value
+ */
+ public int getValue() {
+ return value;
+ }
+
+ /**
+ * Set current value of the scroll.
+ *
+ * @param value the new scroll value
+ */
+ public void setValue(final int value) {
+ this.value = value;
+ }
+
+ /**
+ * Get the increment for clicking on an arrow.
+ *
+ * @return the increment value
+ */
+ public int getSmallChange() {
+ return smallChange;
+ }
+
+ /**
+ * Set the increment for clicking on an arrow.
+ *
+ * @param smallChange the new increment value
+ */
+ public void setSmallChange(final int smallChange) {
+ this.smallChange = smallChange;
+ }
+
+ /**
+ * Set the increment for clicking in the bar between the box and an
+ * arrow.
+ *
+ * @return the increment value
+ */
+ public int getBigChange() {
+ return bigChange;
+ }
+
+ /**
+ * Set the increment for clicking in the bar between the box and an
+ * arrow.
+ *
+ * @param bigChange the new increment value
+ */
+ public void setBigChange(final int bigChange) {
+ this.bigChange = bigChange;
+ }
+
+ /**
+ * Compute the position of the scroll box (a.k.a. grip, thumb).
+ *
+ * @return Y position of the box, between 1 and height - 2
+ */
+ private int boxPosition() {
+ return (getHeight() - 3) * (value - topValue) / (bottomValue - topValue) + 1;
+ }
+
+ /**
+ * Perform a small step change up.
+ */
+ public void decrement() {
+ if (bottomValue == topValue) {
+ return;
+ }
+ value -= smallChange;
+ if (value < topValue) {
+ value = topValue;
+ }
+ }
+
+ /**
+ * Perform a small step change down.
+ */
+ public void increment() {
+ if (bottomValue == topValue) {
+ return;
+ }
+ value += smallChange;
+ if (value > bottomValue) {
+ value = bottomValue;
+ }
+ }
+
+ /**
+ * Perform a big step change up.
+ */
+ public void bigDecrement() {
+ if (bottomValue == topValue) {
+ return;
+ }
+ value -= bigChange;
+ if (value < topValue) {
+ value = topValue;
+ }
+ }
+
+ /**
+ * Perform a big step change down.
+ */
+ public void bigIncrement() {
+ if (bottomValue == topValue) {
+ return;
+ }
+ value += bigChange;
+ if (value > bottomValue) {
+ value = bottomValue;
+ }
+ }
+
+ /**
+ * Go to the top edge of the scroller.
+ */
+ public void toTop() {
+ value = topValue;
+ }
+
+ /**
+ * Go to the bottom edge of the scroller.
+ */
+ public void toBottom() {
+ value = bottomValue;
+ }
+
+ }
--- /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.awt.image.BufferedImage;
+ import java.io.IOException;
+ import java.util.List;
+ import java.util.ArrayList;
+
+ import jexer.backend.Screen;
+ import jexer.bits.Cell;
+ import jexer.bits.CellAttributes;
+ import jexer.bits.ColorTheme;
+ import jexer.event.TCommandEvent;
+ import jexer.event.TInputEvent;
+ import jexer.event.TKeypressEvent;
+ import jexer.event.TMenuEvent;
+ import jexer.event.TMouseEvent;
+ import jexer.event.TResizeEvent;
+ import jexer.layout.LayoutManager;
+ import jexer.menu.TMenu;
+ import jexer.ttree.TTreeItem;
+ import jexer.ttree.TTreeView;
+ import jexer.ttree.TTreeViewWidget;
+ import static jexer.TKeypress.*;
+
+ /**
+ * TWidget is the base class of all objects that can be drawn on screen or
+ * handle user input events.
+ */
+ public abstract class TWidget implements Comparable<TWidget> {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Every widget has a parent widget that it may be "contained" in. For
+ * example, a TWindow might contain several TFields, or a TComboBox may
+ * contain a TList that itself contains a TVScroller.
+ */
+ private TWidget parent = null;
+
+ /**
+ * Child widgets that this widget contains.
+ */
+ private List<TWidget> children;
+
+ /**
+ * The currently active child widget that will receive keypress events.
+ */
+ private TWidget activeChild = null;
+
+ /**
+ * If true, this widget will receive events.
+ */
+ private boolean active = false;
+
+ /**
+ * The window that this widget draws to.
+ */
+ private TWindow window = null;
+
+ /**
+ * Absolute X position of the top-left corner.
+ */
+ private int x = 0;
+
+ /**
+ * Absolute Y position of the top-left corner.
+ */
+ private int y = 0;
+
+ /**
+ * Width.
+ */
+ private int width = 0;
+
+ /**
+ * Height.
+ */
+ private int height = 0;
+
+ /**
+ * My tab order inside a window or containing widget.
+ */
+ private int tabOrder = 0;
+
+ /**
+ * If true, this widget can be tabbed to or receive events.
+ */
+ private boolean enabled = true;
+
+ /**
+ * If true, this widget will be rendered.
+ */
+ private boolean visible = true;
+
+ /**
+ * If true, this widget has a cursor.
+ */
+ private boolean cursorVisible = false;
+
+ /**
+ * Cursor column position in relative coordinates.
+ */
+ private int cursorX = 0;
+
+ /**
+ * Cursor row position in relative coordinates.
+ */
+ private int cursorY = 0;
+
+ /**
+ * Layout manager.
+ */
+ private LayoutManager layout = null;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Default constructor for subclasses.
+ */
+ protected TWidget() {
+ children = new ArrayList<TWidget>();
+ }
+
+ /**
+ * Protected constructor.
+ *
+ * @param parent parent widget
+ */
+ protected TWidget(final TWidget parent) {
+ this(parent, true);
+ }
+
+ /**
+ * Protected constructor.
+ *
+ * @param parent parent widget
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of widget
+ * @param height height of widget
+ */
+ protected TWidget(final TWidget parent, final int x, final int y,
+ final int width, final int height) {
+
+ this(parent, true, x, y, width, height);
+ }
+
+ /**
+ * Protected constructor used by subclasses that are disabled by default.
+ *
+ * @param parent parent widget
+ * @param enabled if true assume enabled
+ */
+ protected TWidget(final TWidget parent, final boolean enabled) {
+ this(parent, enabled, 0, 0, 0, 0);
+ }
+
+ /**
+ * Protected constructor used by subclasses that are disabled by default.
+ *
+ * @param parent parent widget
+ * @param enabled if true assume enabled
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of widget
+ * @param height height of widget
+ */
+ protected TWidget(final TWidget parent, final boolean enabled,
+ final int x, final int y, final int width, final int height) {
+
+ if (width < 0) {
+ throw new IllegalArgumentException("width cannot be negative");
+ }
+ if (height < 0) {
+ throw new IllegalArgumentException("height cannot be negative");
+ }
+
+ this.enabled = enabled;
+ this.parent = parent;
+ children = new ArrayList<TWidget>();
+
+ this.x = x;
+ this.y = y;
+ this.width = width;
+ this.height = height;
+
+ if (parent != null) {
+ this.window = parent.window;
+ parent.addChild(this);
+ }
+ }
+
+ /**
+ * Backdoor access for TWindow's constructor. ONLY TWindow USES THIS.
+ *
+ * @param window the top-level window
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of window
+ * @param height height of window
+ */
+ protected final void setupForTWindow(final TWindow window,
+ final int x, final int y, final int width, final int height) {
+
+ if (width < 0) {
+ throw new IllegalArgumentException("width cannot be negative");
+ }
+ if (height < 0) {
+ throw new IllegalArgumentException("height cannot be negative");
+ }
+
+ this.parent = window;
+ this.window = window;
+ this.x = x;
+ this.y = y;
+ this.width = width;
+ this.height = height;
+ }
+
+ // ------------------------------------------------------------------------
+ // Event handlers ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Subclasses should override this method to cleanup resources. This is
+ * called by TWindow.onClose().
+ */
+ protected void close() {
+ // Default: call close() on children.
+ for (TWidget w: getChildren()) {
+ w.close();
+ }
+ }
+
+ /**
+ * Check if a mouse press/release event coordinate is contained in this
+ * widget.
+ *
+ * @param mouse a mouse-based event
+ * @return whether or not a mouse click would be sent to this widget
+ */
+ public final boolean mouseWouldHit(final TMouseEvent mouse) {
+
+ if (!enabled) {
+ return false;
+ }
+
+ if ((this instanceof TTreeItem)
+ && ((y < 0) || (y > parent.getHeight() - 1))
+ ) {
+ return false;
+ }
+
+ if ((mouse.getAbsoluteX() >= getAbsoluteX())
+ && (mouse.getAbsoluteX() < getAbsoluteX() + width)
+ && (mouse.getAbsoluteY() >= getAbsoluteY())
+ && (mouse.getAbsoluteY() < getAbsoluteY() + height)
+ ) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Method that subclasses can override to handle keystrokes.
+ *
+ * @param keypress keystroke event
+ */
+ public void onKeypress(final TKeypressEvent keypress) {
+ assert (parent != null);
+
+ if ((children.size() == 0)
+ || (this instanceof TTreeView)
+ || (this instanceof TText)
+ || (this instanceof TComboBox)
+ ) {
+
+ // Defaults:
+ // tab / shift-tab - switch to next/previous widget
+ // left-arrow or up-arrow: same as shift-tab
+ if ((keypress.equals(kbTab))
+ || (keypress.equals(kbDown) && !(this instanceof TComboBox))
+ ) {
+ parent.switchWidget(true);
+ return;
+ } else if ((keypress.equals(kbShiftTab))
+ || (keypress.equals(kbBackTab))
+ || (keypress.equals(kbUp) && !(this instanceof TComboBox))
+ ) {
+ parent.switchWidget(false);
+ return;
+ }
+ }
+
+ if ((children.size() == 0)
+ && !(this instanceof TTreeView)
+ ) {
+
+ // Defaults:
+ // right-arrow or down-arrow: same as tab
+ if (keypress.equals(kbRight)) {
+ parent.switchWidget(true);
+ return;
+ } else if (keypress.equals(kbLeft)) {
+ parent.switchWidget(false);
+ return;
+ }
+ }
+
+ // If I have any buttons on me AND this is an Alt-key that matches
+ // its mnemonic, send it an Enter keystroke.
+ for (TWidget widget: children) {
+ if (widget instanceof TButton) {
+ TButton button = (TButton) widget;
+ if (button.isEnabled()
+ && !keypress.getKey().isFnKey()
+ && keypress.getKey().isAlt()
+ && !keypress.getKey().isCtrl()
+ && (Character.toLowerCase(button.getMnemonic().getShortcut())
+ == Character.toLowerCase(keypress.getKey().getChar()))
+ ) {
+
+ widget.onKeypress(new TKeypressEvent(kbEnter));
+ return;
+ }
+ }
+ }
+
+ // If I have any labels on me AND this is an Alt-key that matches
+ // its mnemonic, call its action.
+ for (TWidget widget: children) {
+ if (widget instanceof TLabel) {
+ TLabel label = (TLabel) widget;
+ if (!keypress.getKey().isFnKey()
+ && keypress.getKey().isAlt()
+ && !keypress.getKey().isCtrl()
+ && (Character.toLowerCase(label.getMnemonic().getShortcut())
+ == Character.toLowerCase(keypress.getKey().getChar()))
+ ) {
+
+ label.dispatch();
+ return;
+ }
+ }
+ }
+
+ // If I have any radiobuttons on me AND this is an Alt-key that
+ // matches its mnemonic, select it and send a Space to it.
+ for (TWidget widget: children) {
+ if (widget instanceof TRadioButton) {
+ TRadioButton button = (TRadioButton) widget;
+ if (button.isEnabled()
+ && !keypress.getKey().isFnKey()
+ && keypress.getKey().isAlt()
+ && !keypress.getKey().isCtrl()
+ && (Character.toLowerCase(button.getMnemonic().getShortcut())
+ == Character.toLowerCase(keypress.getKey().getChar()))
+ ) {
+ activate(widget);
+ widget.onKeypress(new TKeypressEvent(kbSpace));
+ return;
+ }
+ }
+ if (widget instanceof TRadioGroup) {
+ for (TWidget child: widget.getChildren()) {
+ if (child instanceof TRadioButton) {
+ TRadioButton button = (TRadioButton) child;
+ if (button.isEnabled()
+ && !keypress.getKey().isFnKey()
+ && keypress.getKey().isAlt()
+ && !keypress.getKey().isCtrl()
+ && (Character.toLowerCase(button.getMnemonic().getShortcut())
+ == Character.toLowerCase(keypress.getKey().getChar()))
+ ) {
+ activate(widget);
+ widget.activate(child);
+ child.onKeypress(new TKeypressEvent(kbSpace));
+ return;
+ }
+ }
+ }
+ }
+ }
+
+ // If I have any checkboxes on me AND this is an Alt-key that matches
+ // its mnemonic, select it and set it to checked.
+ for (TWidget widget: children) {
+ if (widget instanceof TCheckBox) {
+ TCheckBox checkBox = (TCheckBox) widget;
+ if (checkBox.isEnabled()
+ && !keypress.getKey().isFnKey()
+ && keypress.getKey().isAlt()
+ && !keypress.getKey().isCtrl()
+ && (Character.toLowerCase(checkBox.getMnemonic().getShortcut())
+ == Character.toLowerCase(keypress.getKey().getChar()))
+ ) {
+ activate(checkBox);
+ checkBox.setChecked(true);
+ return;
+ }
+ }
+ }
+
+ // Dispatch the keypress to an active widget
+ for (TWidget widget: children) {
+ if (widget.active) {
+ widget.onKeypress(keypress);
+ return;
+ }
+ }
+ }
+
+ /**
+ * Method that subclasses can override to handle mouse button presses.
+ *
+ * @param mouse mouse button event
+ */
+ public void onMouseDown(final TMouseEvent mouse) {
+ // Default: do nothing, pass to children instead
+ if (activeChild != null) {
+ if (activeChild.mouseWouldHit(mouse)) {
+ // Dispatch to the active child
+
+ // Set x and y relative to the child's coordinates
+ mouse.setX(mouse.getAbsoluteX() - activeChild.getAbsoluteX());
+ mouse.setY(mouse.getAbsoluteY() - activeChild.getAbsoluteY());
+ activeChild.onMouseDown(mouse);
+ return;
+ }
+ }
+ for (int i = children.size() - 1 ; i >= 0 ; i--) {
+ TWidget widget = children.get(i);
+ if (widget.mouseWouldHit(mouse)) {
+ // Dispatch to this child, also activate it
+ activate(widget);
+
+ // Set x and y relative to the child's coordinates
+ mouse.setX(mouse.getAbsoluteX() - widget.getAbsoluteX());
+ mouse.setY(mouse.getAbsoluteY() - widget.getAbsoluteY());
+ widget.onMouseDown(mouse);
+ return;
+ }
+ }
+ }
+
+ /**
+ * Method that subclasses can override to handle mouse button releases.
+ *
+ * @param mouse mouse button event
+ */
+ public void onMouseUp(final TMouseEvent mouse) {
+ // Default: do nothing, pass to children instead
+ if (activeChild != null) {
+ if (activeChild.mouseWouldHit(mouse)) {
+ // Dispatch to the active child
+
+ // Set x and y relative to the child's coordinates
+ mouse.setX(mouse.getAbsoluteX() - activeChild.getAbsoluteX());
+ mouse.setY(mouse.getAbsoluteY() - activeChild.getAbsoluteY());
+ activeChild.onMouseUp(mouse);
+ return;
+ }
+ }
+ for (int i = children.size() - 1 ; i >= 0 ; i--) {
+ TWidget widget = children.get(i);
+ if (widget.mouseWouldHit(mouse)) {
+ // Dispatch to this child, also activate it
+ activate(widget);
+
+ // Set x and y relative to the child's coordinates
+ mouse.setX(mouse.getAbsoluteX() - widget.getAbsoluteX());
+ mouse.setY(mouse.getAbsoluteY() - widget.getAbsoluteY());
+ widget.onMouseUp(mouse);
+ return;
+ }
+ }
+ }
+
+ /**
+ * Method that subclasses can override to handle mouse movements.
+ *
+ * @param mouse mouse motion event
+ */
+ public void onMouseMotion(final TMouseEvent mouse) {
+ // Default: do nothing, pass it on to ALL of my children. This way
+ // the children can see the mouse "leaving" their area.
+ for (TWidget widget: children) {
+ // Set x and y relative to the child's coordinates
+ mouse.setX(mouse.getAbsoluteX() - widget.getAbsoluteX());
+ mouse.setY(mouse.getAbsoluteY() - widget.getAbsoluteY());
+ widget.onMouseMotion(mouse);
+ }
+ }
+
+ /**
+ * Method that subclasses can override to handle mouse button
+ * double-clicks.
+ *
+ * @param mouse mouse button event
+ */
+ public void onMouseDoubleClick(final TMouseEvent mouse) {
+ // Default: do nothing, pass to children instead
+ if (activeChild != null) {
+ if (activeChild.mouseWouldHit(mouse)) {
+ // Dispatch to the active child
+
+ // Set x and y relative to the child's coordinates
+ mouse.setX(mouse.getAbsoluteX() - activeChild.getAbsoluteX());
+ mouse.setY(mouse.getAbsoluteY() - activeChild.getAbsoluteY());
+ activeChild.onMouseDoubleClick(mouse);
+ return;
+ }
+ }
+ for (int i = children.size() - 1 ; i >= 0 ; i--) {
+ TWidget widget = children.get(i);
+ if (widget.mouseWouldHit(mouse)) {
+ // Dispatch to this child, also activate it
+ activate(widget);
+
+ // Set x and y relative to the child's coordinates
+ mouse.setX(mouse.getAbsoluteX() - widget.getAbsoluteX());
+ mouse.setY(mouse.getAbsoluteY() - widget.getAbsoluteY());
+ widget.onMouseDoubleClick(mouse);
+ return;
+ }
+ }
+ }
+
+ /**
+ * Method that subclasses can override to handle window/screen resize
+ * events.
+ *
+ * @param resize resize event
+ */
+ public void onResize(final TResizeEvent resize) {
+ // Default: change my width/height.
+ if (resize.getType() == TResizeEvent.Type.WIDGET) {
+ width = resize.getWidth();
+ height = resize.getHeight();
+ if (layout != null) {
+ if (this instanceof TWindow) {
+ layout.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
+ width - 2, height - 2));
+ } else {
+ layout.onResize(resize);
+ }
+ }
+ } else {
+ // Let children see the screen resize
+ for (TWidget widget: children) {
+ widget.onResize(resize);
+ }
+ }
+ }
+
+ /**
+ * Method that subclasses can override to handle posted command events.
+ *
+ * @param command command event
+ */
+ public void onCommand(final TCommandEvent command) {
+ // Default: do nothing, pass to children instead
+ for (TWidget widget: children) {
+ widget.onCommand(command);
+ }
+ }
+
+ /**
+ * Method that subclasses can override to handle menu or posted menu
+ * events.
+ *
+ * @param menu menu event
+ */
+ public void onMenu(final TMenuEvent menu) {
+ // Default: do nothing, pass to children instead
+ for (TWidget widget: children) {
+ widget.onMenu(menu);
+ }
+ }
+
+ /**
+ * Method that subclasses can override to do processing when the UI is
+ * idle. Note that repainting is NOT assumed. To get a refresh after
+ * onIdle, call doRepaint().
+ */
+ public void onIdle() {
+ // Default: do nothing, pass to children instead
+ for (TWidget widget: children) {
+ widget.onIdle();
+ }
+ }
+
+ /**
+ * Consume event. Subclasses that want to intercept all events in one go
+ * can override this method.
+ *
+ * @param event keyboard, mouse, resize, command, or menu event
+ */
+ public void handleEvent(final TInputEvent event) {
+ /*
+ System.err.printf("TWidget (%s) event: %s\n", this.getClass().getName(),
+ event);
+ */
+
+ if (!enabled) {
+ // Discard event
+ // System.err.println(" -- discard --");
+ return;
+ }
+
+ if (event instanceof TKeypressEvent) {
+ onKeypress((TKeypressEvent) event);
+ } else if (event instanceof TMouseEvent) {
+
+ TMouseEvent mouse = (TMouseEvent) event;
+
+ switch (mouse.getType()) {
+
+ case MOUSE_DOWN:
+ onMouseDown(mouse);
+ break;
+
+ case MOUSE_UP:
+ onMouseUp(mouse);
+ break;
+
+ case MOUSE_MOTION:
+ onMouseMotion(mouse);
+ break;
+
+ case MOUSE_DOUBLE_CLICK:
+ onMouseDoubleClick(mouse);
+ break;
+
+ default:
+ throw new IllegalArgumentException("Invalid mouse event type: "
+ + mouse.getType());
+ }
+ } else if (event instanceof TResizeEvent) {
+ onResize((TResizeEvent) event);
+ } else if (event instanceof TCommandEvent) {
+ onCommand((TCommandEvent) event);
+ } else if (event instanceof TMenuEvent) {
+ onMenu((TMenuEvent) event);
+ }
+
+ // Do nothing else
+ return;
+ }
+
+ // ------------------------------------------------------------------------
+ // TWidget ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get parent widget.
+ *
+ * @return parent widget
+ */
+ public final TWidget getParent() {
+ return parent;
+ }
+
+ /**
+ * Get the list of child widgets that this widget contains.
+ *
+ * @return the list of child widgets
+ */
+ public List<TWidget> getChildren() {
+ return children;
+ }
+
+ /**
+ * Remove this widget from its parent container. close() will be called
+ * before it is removed.
+ */
+ public final void remove() {
+ remove(true);
+ }
+
+ /**
+ * Remove this widget from its parent container.
+ *
+ * @param doClose if true, call the close() method before removing the
+ * child
+ */
+ public final void remove(final boolean doClose) {
+ if (parent != null) {
+ parent.remove(this, doClose);
+ }
+ }
+
+ /**
+ * Remove a child widget from this container.
+ *
+ * @param child the child widget to remove
+ */
+ public final void remove(final TWidget child) {
+ remove(child, true);
+ }
+
+ /**
+ * Remove a child widget from this container.
+ *
+ * @param child the child widget to remove
+ * @param doClose if true, call the close() method before removing the
+ * child
+ */
+ public final void remove(final TWidget child, final boolean doClose) {
+ if (!children.contains(child)) {
+ throw new IndexOutOfBoundsException("child widget is not in " +
+ "list of children of this parent");
+ }
+ if (doClose) {
+ child.close();
+ }
+ children.remove(child);
+ child.parent = null;
+ child.window = null;
+ if (layout != null) {
+ layout.remove(this);
+ }
+ }
+
+ /**
+ * Set this widget's parent to a different widget.
+ *
+ * @param newParent new parent widget
+ * @param doClose if true, call the close() method before removing the
+ * child from its existing parent widget
+ */
+ public final void setParent(final TWidget newParent,
+ final boolean doClose) {
+
+ if (parent != null) {
+ parent.remove(this, doClose);
+ window = null;
+ }
+ assert (parent == null);
+ assert (window == null);
+ parent = newParent;
+ setWindow(parent.window);
+ parent.addChild(this);
+ }
+
+ /**
+ * Set this widget's window to a specific window.
+ *
+ * Having a null parent with a specified window is only used within Jexer
+ * by TStatusBar because TApplication routes events directly to it and
+ * calls its draw() method. Any other non-parented widgets will require
+ * similar special case functionality to receive events or be drawn to
+ * screen.
+ *
+ * @param window the window to use
+ */
+ public final void setWindow(final TWindow window) {
+ this.window = window;
+ for (TWidget child: getChildren()) {
+ child.setWindow(window);
+ }
+ }
+
+ /**
+ * Remove a child widget from this container, and all of its children
+ * recursively from their parent containers.
+ *
+ * @param child the child widget to remove
+ * @param doClose if true, call the close() method before removing each
+ * child
+ */
+ public final void removeAll(final TWidget child, final boolean doClose) {
+ remove(child, doClose);
+ for (TWidget w: child.children) {
+ child.removeAll(w, doClose);
+ }
+ }
+
+ /**
+ * Get active flag.
+ *
+ * @return if true, this widget will receive events
+ */
+ public final boolean isActive() {
+ return active;
+ }
+
+ /**
+ * Set active flag.
+ *
+ * @param active if true, this widget will receive events
+ */
+ public final void setActive(final boolean active) {
+ this.active = active;
+ }
+
+ /**
+ * Get the window this widget is on.
+ *
+ * @return the window
+ */
+ public final TWindow getWindow() {
+ return window;
+ }
+
+ /**
+ * Get X position.
+ *
+ * @return absolute X position of the top-left corner
+ */
+ public final int getX() {
+ return x;
+ }
+
+ /**
+ * Set X position.
+ *
+ * @param x absolute X position of the top-left corner
+ */
+ public final void setX(final int x) {
+ this.x = x;
+ }
+
+ /**
+ * Get Y position.
+ *
+ * @return absolute Y position of the top-left corner
+ */
+ public final int getY() {
+ return y;
+ }
+
+ /**
+ * Set Y position.
+ *
+ * @param y absolute Y position of the top-left corner
+ */
+ public final void setY(final int y) {
+ this.y = y;
+ }
+
+ /**
+ * Get the width.
+ *
+ * @return widget width
+ */
+ public int getWidth() {
+ return this.width;
+ }
+
+ /**
+ * Change the width.
+ *
+ * @param width new widget width
+ */
+ public void setWidth(final int width) {
+ this.width = width;
+ if (layout != null) {
+ layout.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
+ width, height));
+ }
+ }
+
+ /**
+ * Get the height.
+ *
+ * @return widget height
+ */
+ public int getHeight() {
+ return this.height;
+ }
+
+ /**
+ * Change the height.
+ *
+ * @param height new widget height
+ */
+ public void setHeight(final int height) {
+ this.height = height;
+ if (layout != null) {
+ layout.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
+ width, height));
+ }
+ }
+
+ /**
+ * Change the dimensions.
+ *
+ * @param x absolute X position of the top-left corner
+ * @param y absolute Y position of the top-left corner
+ * @param width new widget width
+ * @param height new widget height
+ */
+ public final void setDimensions(final int x, final int y, final int width,
+ final int height) {
+
+ this.x = x;
+ this.y = y;
+ // Call the functions so that subclasses can choose how to handle it.
+ setWidth(width);
+ setHeight(height);
+ if (layout != null) {
+ layout.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
+ width, height));
+ }
+ }
+
+ /**
+ * Get the layout manager.
+ *
+ * @return the layout manager, or null if not set
+ */
+ public LayoutManager getLayoutManager() {
+ return layout;
+ }
+
+ /**
+ * Set the layout manager.
+ *
+ * @param layout the new layout manager
+ */
+ public void setLayoutManager(LayoutManager layout) {
+ if (this.layout != null) {
+ for (TWidget w: children) {
+ this.layout.remove(w);
+ }
+ this.layout = null;
+ }
+ this.layout = layout;
+ if (this.layout != null) {
+ for (TWidget w: children) {
+ this.layout.add(w);
+ }
+ }
+ }
+
+ /**
+ * Get enabled flag.
+ *
+ * @return if true, this widget can be tabbed to or receive events
+ */
+ public final boolean isEnabled() {
+ return enabled;
+ }
+
+ /**
+ * Set enabled flag.
+ *
+ * @param enabled if true, this widget can be tabbed to or receive events
+ */
+ public final void setEnabled(final boolean enabled) {
+ this.enabled = enabled;
+ if (!enabled) {
+ active = false;
+ // See if there are any active siblings to switch to
+ boolean foundSibling = false;
+ if (parent != null) {
+ for (TWidget w: parent.children) {
+ if ((w.enabled)
+ && !(this instanceof THScroller)
+ && !(this instanceof TVScroller)
+ ) {
+ parent.activate(w);
+ foundSibling = true;
+ break;
+ }
+ }
+ if (!foundSibling) {
+ parent.activeChild = null;
+ }
+ }
+ }
+ }
+
+ /**
+ * Set visible flag.
+ *
+ * @param visible if true, this widget will be drawn
+ */
+ public final void setVisible(final boolean visible) {
+ this.visible = visible;
+ }
+
+ /**
+ * See if this widget is visible.
+ *
+ * @return if true, this widget will be drawn
+ */
+ public final boolean isVisible() {
+ return visible;
+ }
+
+ /**
+ * Set visible cursor flag.
+ *
+ * @param cursorVisible if true, this widget has a cursor
+ */
+ public final void setCursorVisible(final boolean cursorVisible) {
+ this.cursorVisible = cursorVisible;
+ }
+
+ /**
+ * See if this widget has a visible cursor.
+ *
+ * @return if true, this widget has a visible cursor
+ */
+ public final boolean isCursorVisible() {
+ // If cursor is out of my bounds, it is not visible.
+ if ((cursorX >= width)
+ || (cursorX < 0)
+ || (cursorY >= height)
+ || (cursorY < 0)
+ ) {
+ return false;
+ }
+
+ assert (window != null);
+
+ if (window instanceof TDesktop) {
+ // Desktop doesn't have a window border.
+ return cursorVisible;
+ }
+
+ // If cursor is out of my window's bounds, it is not visible.
+ if ((getCursorAbsoluteX() >= window.getAbsoluteX()
+ + window.getWidth() - 1)
+ || (getCursorAbsoluteX() < 0)
+ || (getCursorAbsoluteY() >= window.getAbsoluteY()
+ + window.getHeight() - 1)
+ || (getCursorAbsoluteY() < 0)
+ ) {
+ return false;
+ }
+ return cursorVisible;
+ }
+
+ /**
+ * Get cursor X value.
+ *
+ * @return cursor column position in relative coordinates
+ */
+ public final int getCursorX() {
+ return cursorX;
+ }
+
+ /**
+ * Set cursor X value.
+ *
+ * @param cursorX column position in relative coordinates
+ */
+ public final void setCursorX(final int cursorX) {
+ this.cursorX = cursorX;
+ }
+
+ /**
+ * Get cursor Y value.
+ *
+ * @return cursor row position in relative coordinates
+ */
+ public final int getCursorY() {
+ return cursorY;
+ }
+
+ /**
+ * Set cursor Y value.
+ *
+ * @param cursorY row position in relative coordinates
+ */
+ public final void setCursorY(final int cursorY) {
+ this.cursorY = cursorY;
+ }
+
+ /**
+ * Get this TWidget's parent TApplication.
+ *
+ * @return the parent TApplication, or null if not assigned
+ */
+ public TApplication getApplication() {
+ if (window != null) {
+ return window.getApplication();
+ }
+ return null;
+ }
+
+ /**
+ * Get the Screen.
+ *
+ * @return the Screen, or null if not assigned
+ */
+ public Screen getScreen() {
+ if (window != null) {
+ return window.getScreen();
+ }
+ return null;
+ }
+
+ /**
+ * Comparison operator. For various subclasses it sorts on:
+ * <ul>
+ * <li>tabOrder for TWidgets</li>
+ * <li>z for TWindows</li>
+ * <li>text for TTreeItems</li>
+ * </ul>
+ *
+ * @param that another TWidget, TWindow, or TTreeItem instance
+ * @return difference between this.tabOrder and that.tabOrder, or
+ * difference between this.z and that.z, or String.compareTo(text)
+ */
+ @Override
+ public final int compareTo(final TWidget that) {
+ if ((this instanceof TWindow)
+ && (that instanceof TWindow)
+ ) {
+ return (((TWindow) this).getZ() - ((TWindow) that).getZ());
+ }
+ if ((this instanceof TTreeItem)
+ && (that instanceof TTreeItem)
+ ) {
+ return (((TTreeItem) this).getText().compareTo(
+ ((TTreeItem) that).getText()));
+ }
+ return (this.tabOrder - that.tabOrder);
+ }
+
+ /**
+ * See if this widget should render with the active color.
+ *
+ * @return true if this widget is active and all of its parents are
+ * active.
+ */
+ public final boolean isAbsoluteActive() {
+ if (parent == this) {
+ return active;
+ }
+ return (active && (parent == null ? true : parent.isAbsoluteActive()));
+ }
+
+ /**
+ * Returns the cursor X position.
+ *
+ * @return absolute screen column number for the cursor's X position
+ */
+ public final int getCursorAbsoluteX() {
+ return getAbsoluteX() + cursorX;
+ }
+
+ /**
+ * Returns the cursor Y position.
+ *
+ * @return absolute screen row number for the cursor's Y position
+ */
+ public final int getCursorAbsoluteY() {
+ return getAbsoluteY() + cursorY;
+ }
+
+ /**
+ * Compute my absolute X position as the sum of my X plus all my parent's
+ * X's.
+ *
+ * @return absolute screen column number for my X position
+ */
+ public final int getAbsoluteX() {
+ assert (parent != null);
+ if (parent == this) {
+ return x;
+ }
+ if ((parent instanceof TWindow)
+ && !(parent instanceof TMenu)
+ && !(parent instanceof TDesktop)
+ ) {
+ // Widgets on a TWindow have (0,0) as their top-left, but this is
+ // actually the TWindow's (1,1).
+ return parent.getAbsoluteX() + x + 1;
+ }
+ return parent.getAbsoluteX() + x;
+ }
+
+ /**
+ * Compute my absolute Y position as the sum of my Y plus all my parent's
+ * Y's.
+ *
+ * @return absolute screen row number for my Y position
+ */
+ public final int getAbsoluteY() {
+ assert (parent != null);
+ if (parent == this) {
+ return y;
+ }
+ if ((parent instanceof TWindow)
+ && !(parent instanceof TMenu)
+ && !(parent instanceof TDesktop)
+ ) {
+ // Widgets on a TWindow have (0,0) as their top-left, but this is
+ // actually the TWindow's (1,1).
+ return parent.getAbsoluteY() + y + 1;
+ }
+ return parent.getAbsoluteY() + y;
+ }
+
+ /**
+ * Get the global color theme.
+ *
+ * @return the ColorTheme
+ */
+ protected final ColorTheme getTheme() {
+ return window.getApplication().getTheme();
+ }
+
+ /**
+ * See if this widget can be drawn onto a screen.
+ *
+ * @return true if this widget is part of the hierarchy that can draw to
+ * a screen
+ */
+ public final boolean isDrawable() {
+ if ((window == null)
+ || (window.getScreen() == null)
+ || (parent == null)
+ ) {
+ return false;
+ }
+ if (parent == this) {
+ return true;
+ }
+ return (parent.isDrawable());
+ }
+
+ /**
+ * Draw my specific widget. When called, the screen rectangle I draw
+ * into is already setup (offset and clipping).
+ */
+ public void draw() {
+ // Default widget draws nothing.
+ }
+
+ /**
+ * Called by parent to render to TWindow. Note package private access.
+ */
+ final void drawChildren() {
+ if (!isDrawable()) {
+ return;
+ }
+
+ // Set my clipping rectangle
+ assert (window != null);
+ assert (getScreen() != null);
+ Screen screen = getScreen();
+
+ // Special case: TStatusBar is drawn by TApplication, not anything
+ // else.
+ if (this instanceof TStatusBar) {
+ return;
+ }
+
+ screen.setClipRight(width);
+ screen.setClipBottom(height);
+
+ int absoluteRightEdge = window.getAbsoluteX() + window.getWidth();
+ int absoluteBottomEdge = window.getAbsoluteY() + window.getHeight();
+ if (!(this instanceof TWindow)
+ && !(this instanceof TVScroller)
+ && !(window instanceof TDesktop)
+ ) {
+ absoluteRightEdge -= 1;
+ }
+ if (!(this instanceof TWindow)
+ && !(this instanceof THScroller)
+ && !(window instanceof TDesktop)
+ ) {
+ absoluteBottomEdge -= 1;
+ }
+ int myRightEdge = getAbsoluteX() + width;
+ int myBottomEdge = getAbsoluteY() + height;
+ if (getAbsoluteX() > absoluteRightEdge) {
+ // I am offscreen
+ screen.setClipRight(0);
+ } else if (myRightEdge > absoluteRightEdge) {
+ screen.setClipRight(screen.getClipRight()
+ - (myRightEdge - absoluteRightEdge));
+ }
+ if (getAbsoluteY() > absoluteBottomEdge) {
+ // I am offscreen
+ screen.setClipBottom(0);
+ } else if (myBottomEdge > absoluteBottomEdge) {
+ screen.setClipBottom(screen.getClipBottom()
+ - (myBottomEdge - absoluteBottomEdge));
+ }
+
+ // Set my offset
+ screen.setOffsetX(getAbsoluteX());
+ screen.setOffsetY(getAbsoluteY());
+
+ // Draw me
+ draw();
+ if (!isDrawable()) {
+ // An action taken by a draw method unhooked me from the UI.
+ // Bail out.
+ return;
+ }
+
+ assert (visible == true);
+
+ // Continue down the chain. Draw the active child last so that it
+ // is on top.
+ for (TWidget widget: children) {
+ if (widget.isVisible() && (widget != activeChild)) {
+ widget.drawChildren();
+ if (!isDrawable()) {
+ // An action taken by a draw method unhooked me from the UI.
+ // Bail out.
+ return;
+ }
+ }
+ }
+ if (activeChild != null) {
+ activeChild.drawChildren();
+ }
+ }
+
+ /**
+ * Repaint the screen on the next update.
+ */
+ protected final void doRepaint() {
+ window.getApplication().doRepaint();
+ }
+
+ /**
+ * Add a child widget to my list of children. We set its tabOrder to 0
+ * and increment the tabOrder of all other children.
+ *
+ * @param child TWidget to add
+ */
+ private void addChild(final TWidget child) {
+ children.add(child);
+
+ if ((child.enabled)
+ && !(child instanceof THScroller)
+ && !(child instanceof TVScroller)
+ ) {
+ for (TWidget widget: children) {
+ widget.active = false;
+ }
+ child.active = true;
+ activeChild = child;
+ }
+ for (int i = 0; i < children.size(); i++) {
+ children.get(i).tabOrder = i;
+ }
+ if (layout != null) {
+ layout.add(child);
+ }
+ }
+
+ /**
+ * Reset the tab order of children to match their position in the list.
+ * Available so that subclasses can re-order their widgets if needed.
+ */
+ protected void resetTabOrder() {
+ for (int i = 0; i < children.size(); i++) {
+ children.get(i).tabOrder = i;
+ }
+ }
+
+ /**
+ * Remove and {@link TWidget#close()} the given child from this {@link TWidget}.
+ * <p>
+ * Will also reorder the tab values of the remaining children.
+ *
+ * @param child the child to remove
+ *
+ * @return TRUE if the child was removed, FALSE if it was not found
+ */
+ public boolean removeChild(final TWidget child) {
+ if (children.remove(child)) {
+ child.close();
+ child.parent = null;
+ child.window = null;
+
+ resetTabOrder();
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Switch the active child.
+ *
+ * @param child TWidget to activate
+ */
+ public final void activate(final TWidget child) {
+ assert (child.enabled);
+ if ((child instanceof THScroller)
+ || (child instanceof TVScroller)
+ ) {
+ return;
+ }
+
+ if (children.size() == 1) {
+ if (children.get(0).enabled == true) {
+ child.active = true;
+ activeChild = child;
+ }
+ } else {
+ if (child != activeChild) {
+ if (activeChild != null) {
+ activeChild.active = false;
+ }
+ child.active = true;
+ activeChild = child;
+ }
+ }
+ }
+
+ /**
+ * Switch the active child.
+ *
+ * @param tabOrder tabOrder of the child to activate. If that child
+ * isn't enabled, then the next enabled child will be activated.
+ */
+ public final void activate(final int tabOrder) {
+ if (children.size() == 1) {
+ if (children.get(0).enabled == true) {
+ children.get(0).active = true;
+ activeChild = children.get(0);
+ }
+ return;
+ }
+
+ TWidget child = null;
+ for (TWidget widget: children) {
+ if ((widget.enabled)
+ && !(widget instanceof THScroller)
+ && !(widget instanceof TVScroller)
+ && (widget.tabOrder >= tabOrder)
+ ) {
+ child = widget;
+ break;
+ }
+ }
+ if ((child != null) && (child != activeChild)) {
+ if (activeChild != null) {
+ activeChild.active = false;
+ }
+ assert (child.enabled);
+ child.active = true;
+ activeChild = child;
+ }
+ }
+
+ /**
+ * Make this widget the active child of its parent. Note that this is
+ * not final since TWindow overrides activate().
+ */
+ public void activate() {
+ if (enabled) {
+ if (parent != null) {
+ parent.activate(this);
+ }
+ }
+ }
+
+ /**
+ * Make this widget, all of its parents, the active child.
+ */
+ public final void activateAll() {
+ activate();
+ if (parent == this) {
+ return;
+ }
+ if (parent != null) {
+ parent.activateAll();
+ }
+ }
+
+ /**
+ * Switch the active widget with the next in the tab order.
+ *
+ * @param forward if true, then switch to the next enabled widget in the
+ * list, otherwise switch to the previous enabled widget in the list
+ */
+ public final void switchWidget(final boolean forward) {
+
+ // No children: do nothing.
+ if (children.size() == 0) {
+ return;
+ }
+
+ assert (parent != null);
+
+ // If there is only one child, make it active if it is enabled.
+ if (children.size() == 1) {
+ if (children.get(0).enabled == true) {
+ activeChild = children.get(0);
+ activeChild.active = true;
+ } else {
+ children.get(0).active = false;
+ activeChild = null;
+ }
+ return;
+ }
+
+ // Two or more children: go forward or backward to the next enabled
+ // child.
+ int tabOrder = 0;
+ if (activeChild != null) {
+ tabOrder = activeChild.tabOrder;
+ }
+ do {
+ if (forward) {
+ tabOrder++;
+ } else {
+ tabOrder--;
+ }
+ if (tabOrder < 0) {
+
+ // If at the end, pass the switch to my parent.
+ if ((!forward) && (parent != this)) {
+ parent.switchWidget(forward);
+ return;
+ }
+
+ tabOrder = children.size() - 1;
+ } else if (tabOrder == children.size()) {
+ // If at the end, pass the switch to my parent.
+ if ((forward) && (parent != this)) {
+ parent.switchWidget(forward);
+ return;
+ }
+
+ tabOrder = 0;
+ }
+ if (activeChild == null) {
+ if (tabOrder == 0) {
+ // We wrapped around
+ break;
+ }
+ } else if (activeChild.tabOrder == tabOrder) {
+ // We wrapped around
+ break;
+ }
+ } while ((!children.get(tabOrder).enabled)
+ && !(children.get(tabOrder) instanceof THScroller)
+ && !(children.get(tabOrder) instanceof TVScroller));
+
+ if (activeChild != null) {
+ assert (children.get(tabOrder).enabled);
+
+ activeChild.active = false;
+ }
+ if (children.get(tabOrder).enabled == true) {
+ children.get(tabOrder).active = true;
+ activeChild = children.get(tabOrder);
+ }
+ }
+
+ /**
+ * Returns my active widget.
+ *
+ * @return widget that is active, or this if no children
+ */
+ public TWidget getActiveChild() {
+ if ((this instanceof THScroller)
+ || (this instanceof TVScroller)
+ ) {
+ return parent;
+ }
+
+ for (TWidget widget: children) {
+ if (widget.active) {
+ return widget.getActiveChild();
+ }
+ }
+ // No active children, return me
+ return this;
+ }
+
+ /**
+ * Insert a vertical split between this widget and parent, and optionally
+ * put another widget in the other side of the split.
+ *
+ * @param newWidgetOnLeft if true, the new widget (if specified) will be
+ * on the left pane, and this widget will be placed on the right pane
+ * @param newWidget the new widget to add to the other pane, or null
+ * @return the new split pane widget
+ */
+ public TSplitPane splitVertical(final boolean newWidgetOnLeft,
+ final TWidget newWidget) {
+
+ TSplitPane splitPane = new TSplitPane(null, x, y, width, height, true);
+ TWidget myParent = parent;
+ remove(false);
+ if (myParent instanceof TSplitPane) {
+ // TSplitPane has a left/right/top/bottom link to me somewhere,
+ // replace it with a link to splitPane.
+ ((TSplitPane) myParent).replaceWidget(this, splitPane);
+ }
+ splitPane.setParent(myParent, false);
+ if (newWidgetOnLeft) {
+ splitPane.setLeft(newWidget);
+ splitPane.setRight(this);
+ } else {
+ splitPane.setLeft(this);
+ splitPane.setRight(newWidget);
+ }
+ if (newWidget != null) {
+ newWidget.activateAll();
+ } else {
+ activateAll();
+ }
+
+ assert (parent != null);
+ assert (window != null);
+ assert (splitPane.getWindow() != null);
+ assert (splitPane.getParent() != null);
+ assert (splitPane.isActive() == true);
+ assert (parent == splitPane);
+ if (newWidget != null) {
+ assert (newWidget.parent == parent);
+ assert (newWidget.active == true);
+ assert (active == false);
+ } else {
+ assert (active == true);
+ }
+ return splitPane;
+ }
+
+ /**
+ * Insert a horizontal split between this widget and parent, and
+ * optionally put another widget in the other side of the split.
+ *
+ * @param newWidgetOnTop if true, the new widget (if specified) will be
+ * on the top pane, and this widget's children will be placed on the
+ * bottom pane
+ * @param newWidget the new widget to add to the other pane, or null
+ * @return the new split pane widget
+ */
+ public TSplitPane splitHorizontal(final boolean newWidgetOnTop,
+ final TWidget newWidget) {
+
+ TSplitPane splitPane = new TSplitPane(null, x, y, width, height, false);
+ TWidget myParent = parent;
+ remove(false);
+ if (myParent instanceof TSplitPane) {
+ // TSplitPane has a left/right/top/bottom link to me somewhere,
+ // replace it with a link to splitPane.
+ ((TSplitPane) myParent).replaceWidget(this, splitPane);
+ }
+ splitPane.setParent(myParent, false);
+ if (newWidgetOnTop) {
+ splitPane.setTop(newWidget);
+ splitPane.setBottom(this);
+ } else {
+ splitPane.setTop(this);
+ splitPane.setBottom(newWidget);
+ }
+ if (newWidget != null) {
+ newWidget.activateAll();
+ } else {
+ activateAll();
+ }
+
+ assert (parent != null);
+ assert (window != null);
+ assert (splitPane.getWindow() != null);
+ assert (splitPane.getParent() != null);
+ assert (splitPane.isActive() == true);
+ assert (parent == splitPane);
+ if (newWidget != null) {
+ assert (newWidget.parent == parent);
+ assert (newWidget.active == true);
+ assert (active == false);
+ } else {
+ assert (active == true);
+ }
+ return splitPane;
+ }
+
+ /**
+ * Generate a human-readable string for this widget.
+ *
+ * @return a human-readable string
+ */
+ @Override
+ public String toString() {
+ return String.format("%s(%8x) position (%d, %d) geometry %dx%d " +
+ "active %s enabled %s visible %s", getClass().getName(),
+ hashCode(), x, y, width, height, active, enabled, visible);
+ }
+
+ /**
+ * Generate a string for this widget's hierarchy.
+ *
+ * @param prefix a prefix to use for this widget's place in the hierarchy
+ * @return a pretty-printable string of this hierarchy
+ */
+ protected String toPrettyString(final String prefix) {
+ StringBuilder sb = new StringBuilder(prefix);
+ sb.append(toString());
+ String newPrefix = "";
+ for (int i = 0; i < prefix.length(); i++) {
+ newPrefix += " ";
+ }
+ for (int i = 0; i < children.size(); i++) {
+ TWidget child= children.get(i);
+ sb.append("\n");
+ if (i == children.size() - 1) {
+ sb.append(child.toPrettyString(newPrefix + " \u2514\u2500"));
+ } else {
+ sb.append(child.toPrettyString(newPrefix + " \u251c\u2500"));
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Generate a string for this widget's hierarchy.
+ *
+ * @return a pretty-printable string of this hierarchy
+ */
+ public String toPrettyString() {
+ return toPrettyString("");
+ }
+
+ // ------------------------------------------------------------------------
+ // Passthru for Screen functions ------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get the attributes at one location.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @return attributes at (x, y)
+ */
+ protected final CellAttributes getAttrXY(final int x, final int y) {
+ return getScreen().getAttrXY(x, y);
+ }
+
+ /**
+ * Set the attributes at one location.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param attr attributes to use (bold, foreColor, backColor)
+ */
+ protected final void putAttrXY(final int x, final int y,
+ final CellAttributes attr) {
+
+ getScreen().putAttrXY(x, y, attr);
+ }
+
+ /**
+ * Set the attributes at one location.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param attr attributes to use (bold, foreColor, backColor)
+ * @param clip if true, honor clipping/offset
+ */
+ protected final void putAttrXY(final int x, final int y,
+ final CellAttributes attr, final boolean clip) {
+
+ getScreen().putAttrXY(x, y, attr, clip);
+ }
+
+ /**
+ * Fill the entire screen with one character with attributes.
+ *
+ * @param ch character to draw
+ * @param attr attributes to use (bold, foreColor, backColor)
+ */
+ protected final void putAll(final int ch, final CellAttributes attr) {
+ getScreen().putAll(ch, attr);
+ }
+
+ /**
+ * Render one character with attributes.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param ch character + attributes to draw
+ */
+ protected final void putCharXY(final int x, final int y, final Cell ch) {
+ getScreen().putCharXY(x, y, ch);
+ }
+
+ /**
+ * Render one character with attributes.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param ch character to draw
+ * @param attr attributes to use (bold, foreColor, backColor)
+ */
+ protected final void putCharXY(final int x, final int y, final int ch,
+ final CellAttributes attr) {
+
+ getScreen().putCharXY(x, y, ch, attr);
+ }
+
+ /**
+ * Render one character without changing the underlying attributes.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param ch character to draw
+ */
+ protected final void putCharXY(final int x, final int y, final int ch) {
+ getScreen().putCharXY(x, y, ch);
+ }
+
+ /**
+ * Render a string. Does not wrap if the string exceeds the line.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param str string to draw
+ * @param attr attributes to use (bold, foreColor, backColor)
+ */
+ protected final void putStringXY(final int x, final int y, final String str,
+ final CellAttributes attr) {
+
+ getScreen().putStringXY(x, y, str, attr);
+ }
+
+ /**
+ * Render a string without changing the underlying attribute. Does not
+ * wrap if the string exceeds the line.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param str string to draw
+ */
+ protected final void putStringXY(final int x, final int y, final String str) {
+ getScreen().putStringXY(x, y, str);
+ }
+
+ /**
+ * Draw a vertical line from (x, y) to (x, y + n).
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param n number of characters to draw
+ * @param ch character to draw
+ * @param attr attributes to use (bold, foreColor, backColor)
+ */
+ protected final void vLineXY(final int x, final int y, final int n,
+ final int ch, final CellAttributes attr) {
+
+ getScreen().vLineXY(x, y, n, ch, attr);
+ }
+
+ /**
+ * Draw a horizontal line from (x, y) to (x + n, y).
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param n number of characters to draw
+ * @param ch character to draw
+ * @param attr attributes to use (bold, foreColor, backColor)
+ */
+ protected final void hLineXY(final int x, final int y, final int n,
+ final int ch, final CellAttributes attr) {
+
+ getScreen().hLineXY(x, y, n, ch, attr);
+ }
+
+ /**
+ * Draw a box with a border and empty background.
+ *
+ * @param left left column of box. 0 is the left-most row.
+ * @param top top row of the box. 0 is the top-most row.
+ * @param right right column of box
+ * @param bottom bottom row of the box
+ * @param border attributes to use for the border
+ * @param background attributes to use for the background
+ */
+ protected final void drawBox(final int left, final int top,
+ final int right, final int bottom,
+ final CellAttributes border, final CellAttributes background) {
+
+ getScreen().drawBox(left, top, right, bottom, border, background);
+ }
+
+ /**
+ * Draw a box with a border and empty background.
+ *
+ * @param left left column of box. 0 is the left-most row.
+ * @param top top row of the box. 0 is the top-most row.
+ * @param right right column of box
+ * @param bottom bottom row of the box
+ * @param border attributes to use for the border
+ * @param background attributes to use for the background
+ * @param borderType if 1, draw a single-line border; if 2, draw a
+ * double-line border; if 3, draw double-line top/bottom edges and
+ * single-line left/right edges (like Qmodem)
+ * @param shadow if true, draw a "shadow" on the box
+ */
+ protected final void drawBox(final int left, final int top,
+ final int right, final int bottom,
+ final CellAttributes border, final CellAttributes background,
+ final int borderType, final boolean shadow) {
+
+ getScreen().drawBox(left, top, right, bottom, border, background,
+ borderType, shadow);
+ }
+
+ /**
+ * Draw a box shadow.
+ *
+ * @param left left column of box. 0 is the left-most row.
+ * @param top top row of the box. 0 is the top-most row.
+ * @param right right column of box
+ * @param bottom bottom row of the box
+ */
+ protected final void drawBoxShadow(final int left, final int top,
+ final int right, final int bottom) {
+
+ getScreen().drawBoxShadow(left, top, right, bottom);
+ }
+
+ // ------------------------------------------------------------------------
+ // Other TWidget constructors ---------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Convenience function to add a label to this container/window.
+ *
+ * @param text label
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @return the new label
+ */
+ public final TLabel addLabel(final String text, final int x, final int y) {
+ return addLabel(text, x, y, "tlabel");
+ }
+
+ /**
+ * Convenience function to add a label to this container/window.
+ *
+ * @param text label
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param action to call when shortcut is pressed
+ * @return the new label
+ */
+ public final TLabel addLabel(final String text, final int x, final int y,
+ final TAction action) {
+
+ return addLabel(text, x, y, "tlabel", action);
+ }
+
+ /**
+ * Convenience function to add a label to this container/window.
+ *
+ * @param text label
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param colorKey ColorTheme key color to use for foreground text.
+ * Default is "tlabel"
+ * @return the new label
+ */
+ public final TLabel addLabel(final String text, final int x, final int y,
+ final String colorKey) {
+
+ return new TLabel(this, text, x, y, colorKey);
+ }
+
+ /**
+ * Convenience function to add a label to this container/window.
+ *
+ * @param text label
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param colorKey ColorTheme key color to use for foreground text.
+ * Default is "tlabel"
+ * @param action to call when shortcut is pressed
+ * @return the new label
+ */
+ public final TLabel addLabel(final String text, final int x, final int y,
+ final String colorKey, final TAction action) {
+
+ return new TLabel(this, text, x, y, colorKey, action);
+ }
+
+ /**
+ * Convenience function to add a label to this container/window.
+ *
+ * @param text label
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param colorKey ColorTheme key color to use for foreground text.
+ * Default is "tlabel"
+ * @param useWindowBackground if true, use the window's background color
+ * @return the new label
+ */
+ public final TLabel addLabel(final String text, final int x, final int y,
+ final String colorKey, final boolean useWindowBackground) {
+
+ return new TLabel(this, text, x, y, colorKey, useWindowBackground);
+ }
+
+ /**
+ * Convenience function to add a label to this container/window.
+ *
+ * @param text label
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param colorKey ColorTheme key color to use for foreground text.
+ * Default is "tlabel"
+ * @param useWindowBackground if true, use the window's background color
+ * @param action to call when shortcut is pressed
+ * @return the new label
+ */
+ public final TLabel addLabel(final String text, final int x, final int y,
+ final String colorKey, final boolean useWindowBackground,
+ final TAction action) {
+
+ return new TLabel(this, text, x, y, colorKey, useWindowBackground,
+ action);
+ }
+
+ /**
+ * Convenience function to add a button to this container/window.
+ *
+ * @param text label on the button
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param action action to call when button is pressed
+ * @return the new button
+ */
+ public final TButton addButton(final String text, final int x, final int y,
+ final TAction action) {
+
+ return new TButton(this, text, x, y, action);
+ }
+
+ /**
+ * Convenience function to add a checkbox to this container/window.
+ *
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param label label to display next to (right of) the checkbox
+ * @param checked initial check state
+ * @return the new checkbox
+ */
+ public final TCheckBox addCheckBox(final int x, final int y,
+ final String label, final boolean checked) {
+
+ return new TCheckBox(this, x, y, label, checked);
+ }
+
+ /**
+ * Convenience function to add a combobox to this container/window.
+ *
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width visible combobox width, including the down-arrow
+ * @param values the possible values for the box, shown in the drop-down
+ * @param valuesIndex the initial index in values, or -1 for no default
+ * value
+ * @param maxValuesHeight the maximum height of the values drop-down when
+ * it is visible
+ * @param updateAction action to call when a new value is selected from
+ * the list or enter is pressed in the edit field
+ * @return the new combobox
+ */
+ public final TComboBox addComboBox(final int x, final int y,
+ final int width, final List<String> values, final int valuesIndex,
+ final int maxValuesHeight, final TAction updateAction) {
+
+ return new TComboBox(this, x, y, width, values, valuesIndex,
+ maxValuesHeight, updateAction);
+ }
+
+ /**
+ * Convenience function to add a spinner to this container/window.
+ *
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param upAction action to call when the up arrow is clicked or pressed
+ * @param downAction action to call when the down arrow is clicked or
+ * pressed
+ * @return the new spinner
+ */
+ public final TSpinner addSpinner(final int x, final int y,
+ final TAction upAction, final TAction downAction) {
+
+ return new TSpinner(this, x, y, upAction, downAction);
+ }
+
+ /**
+ * Convenience function to add a calendar to this container/window.
+ *
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param updateAction action to call when the user changes the value of
+ * the calendar
+ * @return the new calendar
+ */
+ public final TCalendar addCalendar(final int x, final int y,
+ final TAction updateAction) {
+
+ return new TCalendar(this, x, y, updateAction);
+ }
+
+ /**
+ * Convenience function to add a progress bar to this container/window.
+ *
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of progress bar
+ * @param value initial value of percent complete
+ * @return the new progress bar
+ */
+ public final TProgressBar addProgressBar(final int x, final int y,
+ final int width, final int value) {
+
+ return new TProgressBar(this, x, y, width, value);
+ }
+
+ /**
+ * 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 label label to display on the group box
+ * @return the new radio button group
+ */
+ public final TRadioGroup addRadioGroup(final int x, final int y,
+ final String label) {
+
+ return new TRadioGroup(this, x, y, label);
+ }
+
+ /**
+ * Convenience function to add a text field to this container/window.
+ *
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width visible text width
+ * @param fixed if true, the text cannot exceed the display width
+ * @return the new text field
+ */
+ public final TField addField(final int x, final int y,
+ final int width, final boolean fixed) {
+
+ return new TField(this, x, y, width, fixed);
+ }
+
+ /**
+ * Convenience function to add a text field to this container/window.
+ *
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width visible text width
+ * @param fixed if true, the text cannot exceed the display width
+ * @param text initial text, default is empty string
+ * @return the new text field
+ */
+ public final TField addField(final int x, final int y,
+ final int width, final boolean fixed, final String text) {
+
+ return new TField(this, x, y, width, fixed, text);
+ }
+
+ /**
+ * Convenience function to add a text field to this container/window.
+ *
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width visible text width
+ * @param fixed if true, the text cannot exceed the display width
+ * @param text initial text, default is empty string
+ * @param enterAction function to call when enter key is pressed
+ * @param updateAction function to call when the text is updated
+ * @return the new text field
+ */
+ public final TField addField(final int x, final int y,
+ final int width, final boolean fixed, final String text,
+ final TAction enterAction, final TAction updateAction) {
+
+ return new TField(this, x, y, width, fixed, text, enterAction,
+ updateAction);
+ }
+
+ /**
+ * Convenience function to add a scrollable text box to this
+ * container/window.
+ *
+ * @param text text on the screen
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of text area
+ * @param height height of text area
+ * @param colorKey ColorTheme key color to use for foreground text
+ * @return the new text box
+ */
+ public final TText addText(final String text, final int x,
+ final int y, final int width, final int height, final String colorKey) {
+
+ return new TText(this, text, x, y, width, height, colorKey);
+ }
+
+ /**
+ * Convenience function to add a scrollable text box to this
+ * container/window.
+ *
+ * @param text text on the screen
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of text area
+ * @param height height of text area
+ * @return the new text box
+ */
+ public final TText addText(final String text, final int x, final int y,
+ final int width, final int height) {
+
+ return new TText(this, text, x, y, width, height, "ttext");
+ }
+
+ /**
+ * Convenience function to add an editable text area box to this
+ * container/window.
+ *
+ * @param text text on the screen
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of text area
+ * @param height height of text area
+ * @return the new text box
+ */
+ public final TEditorWidget addEditor(final String text, final int x,
+ final int y, final int width, final int height) {
+
+ return new TEditorWidget(this, text, x, y, width, height);
+ }
+
+ /**
+ * Convenience function to spawn a message box.
+ *
+ * @param title window title, will be centered along the top border
+ * @param caption message to display. Use embedded newlines to get a
+ * multi-line box.
+ * @return the new message box
+ */
+ public final TMessageBox messageBox(final String title,
+ final String caption) {
+
+ return getApplication().messageBox(title, caption, TMessageBox.Type.OK);
+ }
+
+ /**
+ * Convenience function to spawn a message box.
+ *
+ * @param title window title, will be centered along the top border
+ * @param caption message to display. Use embedded newlines to get a
+ * multi-line box.
+ * @param type one of the TMessageBox.Type constants. Default is
+ * Type.OK.
+ * @return the new message box
+ */
+ public final TMessageBox messageBox(final String title,
+ final String caption, final TMessageBox.Type type) {
+
+ return getApplication().messageBox(title, caption, type);
+ }
+
+ /**
+ * Convenience function to spawn an input box.
+ *
+ * @param title window title, will be centered along the top border
+ * @param caption message to display. Use embedded newlines to get a
+ * multi-line box.
+ * @return the new input box
+ */
+ public final TInputBox inputBox(final String title, final String caption) {
+
+ return getApplication().inputBox(title, caption);
+ }
+
+ /**
+ * Convenience function to spawn an input box.
+ *
+ * @param title window title, will be centered along the top border
+ * @param caption message to display. Use embedded newlines to get a
+ * multi-line box.
+ * @param text initial text to seed the field with
+ * @return the new input box
+ */
+ public final TInputBox inputBox(final String title, final String caption,
+ final String text) {
+
+ return getApplication().inputBox(title, caption, text);
+ }
+
+ /**
+ * Convenience function to spawn an input box.
+ *
+ * @param title window title, will be centered along the top border
+ * @param caption message to display. Use embedded newlines to get a
+ * multi-line box.
+ * @param text initial text to seed the field with
+ * @param type one of the Type constants. Default is Type.OK.
+ * @return the new input box
+ */
+ public final TInputBox inputBox(final String title, final String caption,
+ final String text, final TInputBox.Type type) {
+
+ return getApplication().inputBox(title, caption, text, type);
+ }
+
+ /**
+ * Convenience function to add a password text field to this
+ * container/window.
+ *
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width visible text width
+ * @param fixed if true, the text cannot exceed the display width
+ * @return the new text field
+ */
+ public final TPasswordField addPasswordField(final int x, final int y,
+ final int width, final boolean fixed) {
+
+ return new TPasswordField(this, x, y, width, fixed);
+ }
+
+ /**
+ * Convenience function to add a password text field to this
+ * container/window.
+ *
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width visible text width
+ * @param fixed if true, the text cannot exceed the display width
+ * @param text initial text, default is empty string
+ * @return the new text field
+ */
+ public final TPasswordField addPasswordField(final int x, final int y,
+ final int width, final boolean fixed, final String text) {
+
+ return new TPasswordField(this, x, y, width, fixed, text);
+ }
+
+ /**
+ * Convenience function to add a password text field to this
+ * container/window.
+ *
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width visible text width
+ * @param fixed if true, the text cannot exceed the display width
+ * @param text initial text, default is empty string
+ * @param enterAction function to call when enter key is pressed
+ * @param updateAction function to call when the text is updated
+ * @return the new text field
+ */
+ public final TPasswordField addPasswordField(final int x, final int y,
+ final int width, final boolean fixed, final String text,
+ final TAction enterAction, final TAction updateAction) {
+
+ return new TPasswordField(this, x, y, width, fixed, text, enterAction,
+ updateAction);
+ }
+
+ /**
+ * Convenience function to add a scrollable tree view to this
+ * container/window.
+ *
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of tree view
+ * @param height height of tree view
+ * @return the new tree view
+ */
+ public final TTreeViewWidget addTreeViewWidget(final int x, final int y,
+ final int width, final int height) {
+
+ return new TTreeViewWidget(this, x, y, width, height);
+ }
+
+ /**
+ * Convenience function to add a scrollable tree view to this
+ * container/window.
+ *
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of tree view
+ * @param height height of tree view
+ * @param action action to perform when an item is selected
+ * @return the new tree view
+ */
+ public final TTreeViewWidget addTreeViewWidget(final int x, final int y,
+ final int width, final int height, final TAction action) {
+
+ return new TTreeViewWidget(this, x, y, width, height, action);
+ }
+
+ /**
+ * Convenience function to spawn a file open box.
+ *
+ * @param path path of selected file
+ * @return the result of the new file open box
+ * @throws IOException if a java.io operation throws
+ */
+ public final String fileOpenBox(final String path) throws IOException {
+ return getApplication().fileOpenBox(path);
+ }
+
+ /**
+ * Convenience function to spawn a file save box.
+ *
+ * @param path path of selected file
+ * @return the result of the new file open box
+ * @throws IOException if a java.io operation throws
+ */
+ public final String fileSaveBox(final String path) throws IOException {
+ return getApplication().fileOpenBox(path, TFileOpenBox.Type.SAVE);
+ }
+
+ /**
+ * Convenience function to spawn a file open box.
+ *
+ * @param path path of selected file
+ * @param type one of the Type constants
+ * @return the result of the new file open box
+ * @throws IOException if a java.io operation throws
+ */
+ public final String fileOpenBox(final String path,
+ final TFileOpenBox.Type type) throws IOException {
+
+ return getApplication().fileOpenBox(path, type);
+ }
+
+ /**
+ * Convenience function to spawn a file open box.
+ *
+ * @param path path of selected file
+ * @param type one of the Type constants
+ * @param filter a string that files must match to be displayed
+ * @return the result of the new file open box
+ * @throws IOException of a java.io operation throws
+ */
+ public final String fileOpenBox(final String path,
+ final TFileOpenBox.Type type, final String filter) throws IOException {
+
+ ArrayList<String> filters = new ArrayList<String>();
+ filters.add(filter);
+
+ return getApplication().fileOpenBox(path, type, filters);
+ }
+
+ /**
+ * Convenience function to spawn a file open box.
+ *
+ * @param path path of selected file
+ * @param type one of the Type constants
+ * @param filters a list of strings that files must match to be displayed
+ * @return the result of the new file open box
+ * @throws IOException of a java.io operation throws
+ */
+ public final String fileOpenBox(final String path,
+ final TFileOpenBox.Type type,
+ final List<String> filters) throws IOException {
+
+ return getApplication().fileOpenBox(path, type, filters);
+ }
+
+ /**
+ * Convenience function to add a directory list to this container/window.
+ *
+ * @param path directory path, must be a directory
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of text area
+ * @param height height of text area
+ * @return the new directory list
+ */
+ public final TDirectoryList addDirectoryList(final String path, final int x,
+ final int y, final int width, final int height) {
+
+ return new TDirectoryList(this, path, x, y, width, height, null);
+ }
+
+ /**
+ * Convenience function to add a directory list to this container/window.
+ *
+ * @param path directory path, must be a directory
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of text area
+ * @param height height of text area
+ * @param action action to perform when an item is selected (enter or
+ * double-click)
+ * @return the new directory list
+ */
+ public final TDirectoryList addDirectoryList(final String path, final int x,
+ final int y, final int width, final int height, final TAction action) {
+
+ return new TDirectoryList(this, path, x, y, width, height, action);
+ }
+
+ /**
+ * Convenience function to add a directory list to this container/window.
+ *
+ * @param path directory path, must be a directory
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of text area
+ * @param height height of text area
+ * @param action action to perform when an item is selected (enter or
+ * double-click)
+ * @param singleClickAction action to perform when an item is selected
+ * (single-click)
+ * @return the new directory list
+ */
+ public final TDirectoryList addDirectoryList(final String path, final int x,
+ final int y, final int width, final int height, final TAction action,
+ final TAction singleClickAction) {
+
+ return new TDirectoryList(this, path, x, y, width, height, action,
+ singleClickAction);
+ }
+
+ /**
+ * Convenience function to add a directory list to this container/window.
+ *
+ * @param path directory path, must be a directory
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of text area
+ * @param height height of text area
+ * @param action action to perform when an item is selected (enter or
+ * double-click)
+ * @param singleClickAction action to perform when an item is selected
+ * (single-click)
+ * @param filters a list of strings that files must match to be displayed
+ * @return the new directory list
+ */
+ public final TDirectoryList addDirectoryList(final String path, final int x,
+ final int y, final int width, final int height, final TAction action,
+ final TAction singleClickAction, final List<String> filters) {
+
+ return new TDirectoryList(this, path, x, y, width, height, action,
+ singleClickAction, filters);
+ }
+
+ /**
+ * Convenience function to add a list to this container/window.
+ *
+ * @param strings list of strings to show
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of text area
+ * @param height height of text area
+ * @return the new directory list
+ */
+ public final TList addList(final List<String> strings, final int x,
+ final int y, final int width, final int height) {
+
+ return new TList(this, strings, x, y, width, height, null);
+ }
+
+ /**
+ * Convenience function to add a list to this container/window.
+ *
+ * @param strings list of strings to show
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of text area
+ * @param height height of text area
+ * @param enterAction action to perform when an item is selected
+ * @return the new directory list
+ */
+ public final TList addList(final List<String> strings, final int x,
+ final int y, final int width, final int height,
+ final TAction enterAction) {
+
+ return new TList(this, strings, x, y, width, height, enterAction);
+ }
+
+ /**
+ * Convenience function to add a list to this container/window.
+ *
+ * @param strings list of strings to show
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of text area
+ * @param height height of text area
+ * @param enterAction action to perform when an item is selected
+ * @param moveAction action to perform when the user navigates to a new
+ * item with arrow/page keys
+ * @return the new directory list
+ */
+ public final TList addList(final List<String> strings, final int x,
+ final int y, final int width, final int height,
+ final TAction enterAction, final TAction moveAction) {
+
+ return new TList(this, strings, x, y, width, height, enterAction,
+ moveAction);
+ }
+
+ /**
+ * Convenience function to add a list to this container/window.
+ *
+ * @param strings list of strings to show. This is allowed to be null
+ * and set later with setList() or by subclasses.
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of text area
+ * @param height height of text area
+ * @param enterAction action to perform when an item is selected
+ * @param moveAction action to perform when the user navigates to a new
+ * item with arrow/page keys
+ * @param singleClickAction action to perform when the user clicks on an
+ * item
+ */
+ public TList addList(final List<String> strings, final int x,
+ final int y, final int width, final int height,
+ final TAction enterAction, final TAction moveAction,
+ final TAction singleClickAction) {
+
+ return new TList(this, strings, x, y, width, height, enterAction,
+ moveAction, singleClickAction);
+ }
+
+
+ /**
+ * Convenience function to add an image to this container/window.
+ *
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width number of text cells for width of the image
+ * @param height number of text cells for height of the image
+ * @param image the image to display
+ * @param left left column of the image. 0 is the left-most column.
+ * @param top top row of the image. 0 is the top-most row.
+ */
+ public final TImage addImage(final int x, final int y,
+ final int width, final int height,
+ final BufferedImage image, final int left, final int top) {
+
+ return new TImage(this, x, y, width, height, image, left, top);
+ }
+
+ /**
+ * Convenience function to add an image to this container/window.
+ *
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width number of text cells for width of the image
+ * @param height number of text cells for height of the image
+ * @param image the image to display
+ * @param left left column of the image. 0 is the left-most column.
+ * @param top top row of the image. 0 is the top-most row.
+ * @param clickAction function to call when mouse is pressed
+ */
+ public final TImage addImage(final int x, final int y,
+ final int width, final int height,
+ final BufferedImage image, final int left, final int top,
+ final TAction clickAction) {
+
+ return new TImage(this, x, y, width, height, image, left, top,
+ clickAction);
+ }
+
+ /**
+ * Convenience function to add an editable 2D data table to this
+ * container/window.
+ *
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of widget
+ * @param height height of widget
+ */
+ public TTableWidget addTable(final int x, final int y, final int width,
+ final int height) {
+
+ return new TTableWidget(this, x, y, width, height);
+ }
+
+ /**
+ * Convenience function to add an editable 2D data table to this
+ * container/window.
+ *
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of widget
+ * @param height height of widget
+ * @param gridColumns number of columns in grid
+ * @param gridRows number of rows in grid
+ */
+ public TTableWidget addTable(final int x, final int y, final int width,
+ final int height, final int gridColumns, final int gridRows) {
+
+ return new TTableWidget(this, x, y, width, height, gridColumns,
+ gridRows);
+ }
+
+ /**
+ * Convenience function to add a panel to this container/window.
+ *
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of text area
+ * @param height height of text area
+ * @return the new panel
+ */
+ public final TPanel addPanel(final int x, final int y, final int width,
+ final int height) {
+
+ return new TPanel(this, x, y, width, height);
+ }
+
+ /**
+ * Convenience function to add a split pane to this container/window.
+ *
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of text area
+ * @param height height of text area
+ * @param vertical if true, split vertically
+ * @return the new split pane
+ */
+ public final TSplitPane addSplitPane(final int x, final int y,
+ final int width, final int height, final boolean vertical) {
+
+ return new TSplitPane(this, x, y, width, height, vertical);
+ }
+
+ }
--- /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.HashSet;
+ import java.util.Set;
+
+ import jexer.backend.Screen;
+ import jexer.bits.CellAttributes;
+ import jexer.bits.GraphicsChars;
+ import jexer.bits.StringUtils;
+ 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 is the top-level container and drawing surface for other widgets.
+ */
+ public class TWindow extends TWidget {
+
+ // ------------------------------------------------------------------------
+ // Constants --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Window is resizable (default yes).
+ */
+ public static final int RESIZABLE = 0x01;
+
+ /**
+ * Window is modal (default no).
+ */
+ public static final int MODAL = 0x02;
+
+ /**
+ * Window is centered (default no).
+ */
+ public static final int CENTERED = 0x04;
+
+ /**
+ * Window has no close box (default no). Window can still be closed via
+ * TApplication.closeWindow() and TWindow.close().
+ */
+ public static final int NOCLOSEBOX = 0x08;
+
+ /**
+ * Window has no maximize box (default no).
+ */
+ public static final int NOZOOMBOX = 0x10;
+
+ /**
+ * Window is placed at absolute position (no smart placement) (default
+ * no).
+ */
+ public static final int ABSOLUTEXY = 0x20;
+
+ /**
+ * Hitting the closebox with the mouse calls TApplication.hideWindow()
+ * rather than TApplication.closeWindow() (default no).
+ */
+ public static final int HIDEONCLOSE = 0x40;
+
+ /**
+ * Menus cannot be used when this window is active (default no).
+ */
+ public static final int OVERRIDEMENU = 0x80;
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Window flags. Note package private access.
+ */
+ int flags = RESIZABLE;
+
+ /**
+ * Window title.
+ */
+ private String title = "";
+
+ /**
+ * Window's parent TApplication.
+ */
+ private TApplication application;
+
+ /**
+ * Z order. Lower number means more in-front.
+ */
+ private int z = 0;
+
+ /**
+ * Window's keyboard shortcuts. Any key in this set will be passed to
+ * the window directly rather than processed through the menu
+ * accelerators.
+ */
+ private Set<TKeypress> keyboardShortcuts = new HashSet<TKeypress>();
+
+ /**
+ * If true, then the user clicked on the title bar and is moving the
+ * window.
+ */
+ protected boolean inWindowMove = false;
+
+ /**
+ * If true, then the user clicked on the bottom right corner and is
+ * resizing the window.
+ */
+ protected boolean inWindowResize = false;
+
+ /**
+ * If true, then the user selected "Size/Move" (or hit Ctrl-F5) and is
+ * resizing/moving the window via the keyboard.
+ */
+ protected boolean inKeyboardResize = false;
+
+ /**
+ * If true, this window is maximized.
+ */
+ private boolean maximized = false;
+
+ /**
+ * Remember mouse state.
+ */
+ protected TMouseEvent mouse;
+
+ // For moving the window. resizing also uses moveWindowMouseX/Y
+ private int moveWindowMouseX;
+ private int moveWindowMouseY;
+ private int oldWindowX;
+ private int oldWindowY;
+
+ // Resizing
+ private int resizeWindowWidth;
+ private int resizeWindowHeight;
+ private int minimumWindowWidth = 10;
+ private int minimumWindowHeight = 2;
+ private int maximumWindowWidth = -1;
+ private int maximumWindowHeight = -1;
+
+ // For maximize/restore
+ private int restoreWindowWidth;
+ private int restoreWindowHeight;
+ private int restoreWindowX;
+ private int restoreWindowY;
+
+ /**
+ * Hidden flag. A hidden window will still have its onIdle() called, and
+ * will also have onClose() called at application exit. Note package
+ * private access: TApplication will force hidden false if a modal window
+ * is active.
+ */
+ boolean hidden = false;
+
+ /**
+ * A window may have a status bar associated with it. TApplication will
+ * draw this status bar last, and will also route events to it first
+ * before the window.
+ */
+ protected TStatusBar statusBar = null;
+
+ /**
+ * A window may request that TApplication NOT draw the mouse cursor over
+ * it by setting this to true. This is currently only used within Jexer
+ * by TTerminalWindow so that only the bottom-most instance of nested
+ * Jexer's draws the mouse within its application window. But perhaps
+ * other applications can use it, so public getter/setter is provided.
+ */
+ private boolean hideMouse = false;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor. Window will be located at (0, 0).
+ *
+ * @param application TApplication that manages this window
+ * @param title window title, will be centered along the top border
+ * @param width width of window
+ * @param height height of window
+ */
+ public TWindow(final TApplication application, final String title,
+ final int width, final int height) {
+
+ this(application, title, 0, 0, width, height, RESIZABLE);
+ }
+
+ /**
+ * Public constructor. Window will be located at (0, 0).
+ *
+ * @param application TApplication that manages this window
+ * @param title window title, will be centered along the top border
+ * @param width width of window
+ * @param height height of window
+ * @param flags bitmask of RESIZABLE, CENTERED, or MODAL
+ */
+ public TWindow(final TApplication application, final String title,
+ final int width, final int height, final int flags) {
+
+ this(application, title, 0, 0, width, height, flags);
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param application TApplication that manages this window
+ * @param title window title, will be centered along the top border
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of window
+ * @param height height of window
+ */
+ public TWindow(final TApplication application, final String title,
+ final int x, final int y, final int width, final int height) {
+
+ this(application, title, x, y, width, height, RESIZABLE);
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param application TApplication that manages this window
+ * @param title window title, will be centered along the top border
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of window
+ * @param height height of window
+ * @param flags mask of RESIZABLE, CENTERED, or MODAL
+ */
+ public TWindow(final TApplication application, final String title,
+ final int x, final int y, final int width, final int height,
+ final int flags) {
+
+ super();
+
+ // I am my own window and parent
+ setupForTWindow(this, x, y + application.getDesktopTop(),
+ width, height);
+
+ // Save fields
+ this.title = title;
+ this.application = application;
+ this.flags = flags;
+
+ // Minimum width/height are 10 and 2
+ assert (width >= 10);
+ assert (getHeight() >= 2);
+
+ // MODAL implies CENTERED
+ if (isModal()) {
+ this.flags |= CENTERED;
+ }
+
+ // Center window if specified
+ center();
+
+ // Add me to the application
+ application.addWindowToApplication(this);
+ }
+
+ // ------------------------------------------------------------------------
+ // Event handlers ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Returns true if the mouse is currently on the close button.
+ *
+ * @return true if mouse is currently on the close button
+ */
+ protected boolean mouseOnClose() {
+ if ((flags & NOCLOSEBOX) != 0) {
+ return false;
+ }
+ if ((mouse != null)
+ && (mouse.getAbsoluteY() == getY())
+ && (mouse.getAbsoluteX() == getX() + 3)
+ ) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the mouse is currently on the maximize/restore button.
+ *
+ * @return true if the mouse is currently on the maximize/restore button
+ */
+ protected boolean mouseOnMaximize() {
+ if ((flags & NOZOOMBOX) != 0) {
+ return false;
+ }
+ if ((mouse != null)
+ && !isModal()
+ && (mouse.getAbsoluteY() == getY())
+ && (mouse.getAbsoluteX() == getX() + getWidth() - 4)
+ ) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the mouse is currently on the resizable lower right
+ * corner.
+ *
+ * @return true if the mouse is currently on the resizable lower right
+ * corner
+ */
+ protected boolean mouseOnResize() {
+ if (((flags & RESIZABLE) != 0)
+ && !isModal()
+ && (mouse != null)
+ && (mouse.getAbsoluteY() == getY() + getHeight() - 1)
+ && ((mouse.getAbsoluteX() == getX() + getWidth() - 1)
+ || (mouse.getAbsoluteX() == getX() + getWidth() - 2))
+ ) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Subclasses should override this method to perform any user prompting
+ * before they are offscreen. Note that unlike other windowing toolkits,
+ * windows can NOT use this function in some manner to avoid being
+ * closed. This is called by application.closeWindow().
+ */
+ protected void onPreClose() {
+ // Default: do nothing.
+ }
+
+ /**
+ * Subclasses should override this method to cleanup resources. This is
+ * called by application.closeWindow().
+ */
+ protected void onClose() {
+ // Default: perform widget-specific cleanup.
+ for (TWidget w: getChildren()) {
+ w.close();
+ }
+ }
+
+ /**
+ * Called by application.switchWindow() when this window gets the
+ * focus, and also by application.addWindow().
+ */
+ protected void onFocus() {
+ // Default: do nothing
+ }
+
+ /**
+ * Called by application.switchWindow() when another window gets the
+ * focus.
+ */
+ protected void onUnfocus() {
+ // Default: do nothing
+ }
+
+ /**
+ * Called by application.hideWindow().
+ */
+ protected void onHide() {
+ // Default: do nothing
+ }
+
+ /**
+ * Called by application.showWindow().
+ */
+ protected void onShow() {
+ // Default: do nothing
+ }
+
+ /**
+ * Handle mouse button presses.
+ *
+ * @param mouse mouse button event
+ */
+ @Override
+ public void onMouseDown(final TMouseEvent mouse) {
+ this.mouse = mouse;
+
+ inKeyboardResize = false;
+ inWindowMove = false;
+ inWindowResize = false;
+
+ if ((mouse.getAbsoluteY() == getY())
+ && mouse.isMouse1()
+ && (getX() <= mouse.getAbsoluteX())
+ && (mouse.getAbsoluteX() < getX() + getWidth())
+ && !mouseOnClose()
+ && !mouseOnMaximize()
+ ) {
+ // Begin moving window
+ inWindowMove = true;
+ moveWindowMouseX = mouse.getAbsoluteX();
+ moveWindowMouseY = mouse.getAbsoluteY();
+ oldWindowX = getX();
+ oldWindowY = getY();
+ if (maximized) {
+ maximized = false;
+ }
+ return;
+ }
+ if (mouseOnResize()) {
+ // Begin window resize
+ inWindowResize = true;
+ moveWindowMouseX = mouse.getAbsoluteX();
+ moveWindowMouseY = mouse.getAbsoluteY();
+ resizeWindowWidth = getWidth();
+ resizeWindowHeight = getHeight();
+ if (maximized) {
+ maximized = false;
+ }
+ return;
+ }
+
+ // Give the shortcut bar a shot at this.
+ if (statusBar != null) {
+ if (statusBar.statusBarMouseDown(mouse)) {
+ return;
+ }
+ }
+
+ // I didn't take it, pass it on to my children
+ super.onMouseDown(mouse);
+ }
+
+ /**
+ * Handle mouse button releases.
+ *
+ * @param mouse mouse button release event
+ */
+ @Override
+ public void onMouseUp(final TMouseEvent mouse) {
+ this.mouse = mouse;
+
+ if ((inWindowMove) && (mouse.isMouse1())) {
+ // Stop moving window
+ inWindowMove = false;
+ return;
+ }
+
+ if ((inWindowResize) && (mouse.isMouse1())) {
+ // Stop resizing window
+ inWindowResize = false;
+ return;
+ }
+
+ if (mouse.isMouse1() && mouseOnClose()) {
+ if ((flags & HIDEONCLOSE) == 0) {
+ // Close window
+ application.closeWindow(this);
+ } else {
+ // Hide window
+ application.hideWindow(this);
+ }
+ return;
+ }
+
+ if ((mouse.getAbsoluteY() == getY())
+ && mouse.isMouse1()
+ && mouseOnMaximize()) {
+ if (maximized) {
+ // Restore
+ restore();
+ } else {
+ // Maximize
+ maximize();
+ }
+ // Pass a resize event to my children
+ onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
+ getWidth(), getHeight()));
+ return;
+ }
+
+ // Give the shortcut bar a shot at this.
+ if (statusBar != null) {
+ if (statusBar.statusBarMouseUp(mouse)) {
+ return;
+ }
+ }
+
+ // I didn't take it, pass it on to my children
+ super.onMouseUp(mouse);
+ }
+
+ /**
+ * Handle mouse movements.
+ *
+ * @param mouse mouse motion event
+ */
+ @Override
+ public void onMouseMotion(final TMouseEvent mouse) {
+ this.mouse = mouse;
+
+ if (inWindowMove) {
+ // Move window over
+ setX(oldWindowX + (mouse.getAbsoluteX() - moveWindowMouseX));
+ setY(oldWindowY + (mouse.getAbsoluteY() - moveWindowMouseY));
+ // Don't cover up the menu bar
+ if (getY() < application.getDesktopTop()) {
+ setY(application.getDesktopTop());
+ }
+ // Don't go below the status bar
+ if (getY() >= application.getDesktopBottom()) {
+ setY(application.getDesktopBottom() - 1);
+ }
+ return;
+ }
+
+ 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));
+ setHeight(resizeWindowHeight + (mouse.getAbsoluteY()
+ - moveWindowMouseY));
+ if (getX() + getWidth() > getScreen().getWidth()) {
+ setWidth(getScreen().getWidth() - getX());
+ }
+ if (getY() + getHeight() > application.getDesktopBottom()) {
+ setY(application.getDesktopBottom() - getHeight() + 1);
+ }
+ // Don't cover up the menu bar
+ if (getY() < application.getDesktopTop()) {
+ setY(application.getDesktopTop());
+ }
+
+ // 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;
+ }
+
+ // Pass a resize event to my children
+ onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
+ getWidth(), getHeight()));
+ return;
+ }
+
+ // Give the shortcut bar a shot at this.
+ if (statusBar != null) {
+ statusBar.statusBarMouseMotion(mouse);
+ }
+
+ // I didn't take it, pass it on to my children
+ super.onMouseMotion(mouse);
+ }
+
+ /**
+ * Handle keystrokes.
+ *
+ * @param keypress keystroke event
+ */
+ @Override
+ public void onKeypress(final TKeypressEvent keypress) {
+
+ if (inWindowMove || inWindowResize) {
+ // ESC or ENTER - Exit size/move
+ if (keypress.equals(kbEsc) || keypress.equals(kbEnter)) {
+ inWindowMove = false;
+ inWindowResize = false;
+ return;
+ }
+ }
+
+ if (inKeyboardResize) {
+
+ // ESC or ENTER - Exit size/move
+ if (keypress.equals(kbEsc) || keypress.equals(kbEnter)) {
+ inKeyboardResize = false;
+ }
+
+ if (keypress.equals(kbLeft)) {
+ if (getX() > 0) {
+ setX(getX() - 1);
+ }
+ }
+ if (keypress.equals(kbRight)) {
+ if (getX() < getScreen().getWidth() - 1) {
+ setX(getX() + 1);
+ }
+ }
+ if (keypress.equals(kbDown)) {
+ if (getY() < application.getDesktopBottom() - 1) {
+ setY(getY() + 1);
+ }
+ }
+ if (keypress.equals(kbUp)) {
+ if (getY() > 1) {
+ setY(getY() - 1);
+ }
+ }
+
+ /*
+ * Only permit keyboard resizing if the window was RESIZABLE.
+ */
+ if ((flags & RESIZABLE) != 0) {
+
+ if (keypress.equals(kbShiftLeft)) {
+ if ((getWidth() > minimumWindowWidth)
+ || (minimumWindowWidth <= 0)
+ ) {
+ setWidth(getWidth() - 1);
+ }
+ }
+ if (keypress.equals(kbShiftRight)) {
+ if ((getWidth() < maximumWindowWidth)
+ || (maximumWindowWidth <= 0)
+ ) {
+ setWidth(getWidth() + 1);
+ }
+ }
+ if (keypress.equals(kbShiftUp)) {
+ if ((getHeight() > minimumWindowHeight)
+ || (minimumWindowHeight <= 0)
+ ) {
+ setHeight(getHeight() - 1);
+ }
+ }
+ if (keypress.equals(kbShiftDown)) {
+ if ((getHeight() < maximumWindowHeight)
+ || (maximumWindowHeight <= 0)
+ ) {
+ setHeight(getHeight() + 1);
+ }
+ }
+
+ // Pass a resize event to my children
+ onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
+ getWidth(), getHeight()));
+
+ } // if ((flags & RESIZABLE) != 0)
+
+ return;
+ }
+
+ // Give the shortcut bar a shot at this.
+ if (statusBar != null) {
+ if (statusBar.statusBarKeypress(keypress)) {
+ return;
+ }
+ }
+
+ // These keystrokes will typically not be seen unless a subclass
+ // overrides onMenu() due to how TApplication dispatches
+ // accelerators.
+
+ if (!(this instanceof TDesktop)) {
+
+ // Ctrl-W - close window
+ if (keypress.equals(kbCtrlW)) {
+ if ((flags & NOCLOSEBOX) == 0) {
+ if ((flags & HIDEONCLOSE) == 0) {
+ // Close window
+ application.closeWindow(this);
+ } else {
+ // Hide window
+ application.hideWindow(this);
+ }
+ }
+ return;
+ }
+
+ // F6 - behave like Alt-TAB
+ if (keypress.equals(kbF6)) {
+ application.switchWindow(true);
+ return;
+ }
+
+ // Shift-F6 - behave like Shift-Alt-TAB
+ if (keypress.equals(kbShiftF6)) {
+ application.switchWindow(false);
+ return;
+ }
+
+ // F5 - zoom
+ if (keypress.equals(kbF5) && ((flags & NOZOOMBOX) == 0)) {
+ if (maximized) {
+ restore();
+ } else {
+ maximize();
+ }
+ }
+
+ // Ctrl-F5 - size/move
+ if (keypress.equals(kbCtrlF5)) {
+ inKeyboardResize = !inKeyboardResize;
+ }
+
+ } // if (!(this instanceof TDesktop))
+
+ // I didn't take it, pass it on to my children
+ super.onKeypress(keypress);
+ }
+
+ /**
+ * Handle posted command events.
+ *
+ * @param command command event
+ */
+ @Override
+ public void onCommand(final TCommandEvent command) {
+
+ // These commands will typically not be seen unless a subclass
+ // overrides onMenu() due to how TApplication dispatches
+ // accelerators.
+
+ if (!(this instanceof TDesktop)) {
+
+ if (command.equals(cmWindowClose)) {
+ if ((flags & NOCLOSEBOX) == 0) {
+ if ((flags & HIDEONCLOSE) == 0) {
+ // Close window
+ application.closeWindow(this);
+ } else {
+ // Hide window
+ application.hideWindow(this);
+ }
+ }
+ return;
+ }
+
+ if (command.equals(cmWindowNext)) {
+ application.switchWindow(true);
+ return;
+ }
+
+ if (command.equals(cmWindowPrevious)) {
+ application.switchWindow(false);
+ return;
+ }
+
+ if (command.equals(cmWindowMove)) {
+ inKeyboardResize = true;
+ return;
+ }
+
+ if (command.equals(cmWindowZoom) && ((flags & NOZOOMBOX) == 0)) {
+ if (maximized) {
+ restore();
+ } else {
+ maximize();
+ }
+ }
+
+ } // if (!(this instanceof TDesktop))
+
+ // I didn't take it, pass it on to my children
+ super.onCommand(command);
+ }
+
+ /**
+ * Handle posted menu events.
+ *
+ * @param menu menu event
+ */
+ @Override
+ public void onMenu(final TMenuEvent menu) {
+
+ if (!(this instanceof TDesktop)) {
+
+ if (menu.getId() == TMenu.MID_WINDOW_CLOSE) {
+ if ((flags & NOCLOSEBOX) == 0) {
+ if ((flags & HIDEONCLOSE) == 0) {
+ // Close window
+ application.closeWindow(this);
+ } else {
+ // Hide window
+ application.hideWindow(this);
+ }
+ }
+ return;
+ }
+
+ if (menu.getId() == TMenu.MID_WINDOW_NEXT) {
+ application.switchWindow(true);
+ return;
+ }
+
+ if (menu.getId() == TMenu.MID_WINDOW_PREVIOUS) {
+ application.switchWindow(false);
+ return;
+ }
+
+ if (menu.getId() == TMenu.MID_WINDOW_MOVE) {
+ inKeyboardResize = true;
+ return;
+ }
+
+ if ((menu.getId() == TMenu.MID_WINDOW_ZOOM)
+ && ((flags & NOZOOMBOX) == 0)
+ ) {
+ if (maximized) {
+ restore();
+ } else {
+ maximize();
+ }
+ return;
+ }
+
+ } // if (!(this instanceof TDesktop))
+
+ // I didn't take it, pass it on to my children
+ super.onMenu(menu);
+ }
+
+ /**
+ * Method that subclasses can override to handle window/screen resize
+ * events.
+ *
+ * @param resize resize event
+ */
+ @Override
+ public void onResize(final TResizeEvent resize) {
+ if (resize.getType() == TResizeEvent.Type.WIDGET) {
+ if (getChildren().size() == 1) {
+ TWidget child = getChildren().get(0);
+ if ((child instanceof TSplitPane)
+ || (child instanceof TPanel)
+ ) {
+ if (this instanceof TDesktop) {
+ child.onResize(new TResizeEvent(
+ TResizeEvent.Type.WIDGET,
+ resize.getWidth(), resize.getHeight()));
+ } else {
+ child.onResize(new TResizeEvent(
+ TResizeEvent.Type.WIDGET,
+ resize.getWidth() - 2, resize.getHeight() - 2));
+ }
+ }
+ return;
+ }
+ }
+
+ // Pass on to TWidget.
+ super.onResize(resize);
+ }
+
+ // ------------------------------------------------------------------------
+ // TWidget ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get this TWindow's parent TApplication.
+ *
+ * @return this TWindow's parent TApplication
+ */
+ @Override
+ public final TApplication getApplication() {
+ return application;
+ }
+
+ /**
+ * Get the Screen.
+ *
+ * @return the Screen
+ */
+ @Override
+ public final Screen getScreen() {
+ return application.getScreen();
+ }
+
+ /**
+ * Called by TApplication.drawChildren() to render on screen.
+ */
+ @Override
+ public void draw() {
+ // Draw the box and background first.
+ CellAttributes border = getBorder();
+ CellAttributes background = getBackground();
+ int borderType = getBorderType();
+
+ drawBox(0, 0, getWidth(), getHeight(), border, background, borderType,
+ true);
+
+ // Draw the title
+ int titleLength = StringUtils.width(title);
+ int titleLeft = (getWidth() - titleLength - 2) / 2;
+ putCharXY(titleLeft, 0, ' ', border);
+ putStringXY(titleLeft + 1, 0, title, border);
+ putCharXY(titleLeft + titleLength + 1, 0, ' ', border);
+
+ if (isActive()) {
+
+ // Draw the close button
+ if ((flags & NOCLOSEBOX) == 0) {
+ putCharXY(2, 0, '[', border);
+ putCharXY(4, 0, ']', border);
+ if (mouseOnClose() && mouse.isMouse1()) {
+ putCharXY(3, 0, GraphicsChars.CP437[0x0F],
+ getBorderControls());
+ } else {
+ putCharXY(3, 0, GraphicsChars.CP437[0xFE],
+ getBorderControls());
+ }
+ }
+
+ // Draw the maximize button
+ if (!isModal() && ((flags & NOZOOMBOX) == 0)) {
+
+ putCharXY(getWidth() - 5, 0, '[', border);
+ putCharXY(getWidth() - 3, 0, ']', border);
+ if (mouseOnMaximize() && mouse.isMouse1()) {
+ putCharXY(getWidth() - 4, 0, GraphicsChars.CP437[0x0F],
+ getBorderControls());
+ } else {
+ if (maximized) {
+ putCharXY(getWidth() - 4, 0, GraphicsChars.CP437[0x12],
+ getBorderControls());
+ } else {
+ putCharXY(getWidth() - 4, 0, GraphicsChars.UPARROW,
+ getBorderControls());
+ }
+ }
+
+ // Draw the resize corner
+ if ((flags & RESIZABLE) != 0) {
+ putCharXY(getWidth() - 2, getHeight() - 1,
+ GraphicsChars.SINGLE_BAR, getBorderControls());
+ putCharXY(getWidth() - 1, getHeight() - 1,
+ GraphicsChars.LRCORNER, getBorderControls());
+ }
+ }
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // TWindow ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get window title.
+ *
+ * @return window title
+ */
+ public final String getTitle() {
+ return title;
+ }
+
+ /**
+ * Set window title.
+ *
+ * @param title new window title
+ */
+ public final void setTitle(final String title) {
+ this.title = title;
+ }
+
+ /**
+ * Get Z order. Lower number means more in-front.
+ *
+ * @return Z value. Lower number means more in-front.
+ */
+ public final int getZ() {
+ return z;
+ }
+
+ /**
+ * Set Z order. Lower number means more in-front.
+ *
+ * @param z the new Z value. Lower number means more in-front.
+ */
+ public final void setZ(final int z) {
+ this.z = z;
+ }
+
+ /**
+ * Add a keypress to be overridden for this window.
+ *
+ * @param key the key to start taking control of
+ */
+ protected void addShortcutKeypress(final TKeypress key) {
+ keyboardShortcuts.add(key);
+ }
+
+ /**
+ * Remove a keypress to be overridden for this window.
+ *
+ * @param key the key to stop taking control of
+ */
+ protected void removeShortcutKeypress(final TKeypress key) {
+ keyboardShortcuts.remove(key);
+ }
+
+ /**
+ * Remove all keypresses to be overridden for this window.
+ */
+ protected void clearShortcutKeypresses() {
+ keyboardShortcuts.clear();
+ }
+
+ /**
+ * Determine if a keypress is overridden for this window.
+ *
+ * @param key the key to check
+ * @return true if this window wants to process this key on its own
+ */
+ public boolean isShortcutKeypress(final TKeypress key) {
+ return keyboardShortcuts.contains(key);
+ }
+
+ /**
+ * Get the window's status bar, or null if it does not have one.
+ *
+ * @return the status bar, or null
+ */
+ public TStatusBar getStatusBar() {
+ return statusBar;
+ }
+
+ /**
+ * Set the window's status bar to a new one.
+ *
+ * @param text the status bar text
+ * @return the status bar
+ */
+ public TStatusBar newStatusBar(final String text) {
+ statusBar = new TStatusBar(this, text);
+ return statusBar;
+ }
+
+ /**
+ * Set the maximum width for this window.
+ *
+ * @param maximumWindowWidth new maximum width
+ */
+ public final void setMaximumWindowWidth(final int maximumWindowWidth) {
+ if ((maximumWindowWidth != -1)
+ && (maximumWindowWidth < minimumWindowWidth + 1)
+ ) {
+ throw new IllegalArgumentException("Maximum window width cannot " +
+ "be smaller than minimum window width + 1");
+ }
+ this.maximumWindowWidth = maximumWindowWidth;
+ }
+
+ /**
+ * Set the minimum width for this window.
+ *
+ * @param minimumWindowWidth new minimum width
+ */
+ public final void setMinimumWindowWidth(final int minimumWindowWidth) {
+ if ((maximumWindowWidth != -1)
+ && (minimumWindowWidth > maximumWindowWidth - 1)
+ ) {
+ throw new IllegalArgumentException("Minimum window width cannot " +
+ "be larger than maximum window width - 1");
+ }
+ this.minimumWindowWidth = minimumWindowWidth;
+ }
+
+ /**
+ * Set the maximum height for this window.
+ *
+ * @param maximumWindowHeight new maximum height
+ */
+ public final void setMaximumWindowHeight(final int maximumWindowHeight) {
+ if ((maximumWindowHeight != -1)
+ && (maximumWindowHeight < minimumWindowHeight + 1)
+ ) {
+ throw new IllegalArgumentException("Maximum window height cannot " +
+ "be smaller than minimum window height + 1");
+ }
+ this.maximumWindowHeight = maximumWindowHeight;
+ }
+
+ /**
+ * Set the minimum height for this window.
+ *
+ * @param minimumWindowHeight new minimum height
+ */
+ public final void setMinimumWindowHeight(final int minimumWindowHeight) {
+ if ((maximumWindowHeight != -1)
+ && (minimumWindowHeight > maximumWindowHeight - 1)
+ ) {
+ throw new IllegalArgumentException("Minimum window height cannot " +
+ "be larger than maximum window height - 1");
+ }
+ this.minimumWindowHeight = minimumWindowHeight;
+ }
+
+ /**
+ * Recenter the window on-screen.
+ */
+ public final void center() {
+ if ((flags & CENTERED) != 0) {
+ if (getWidth() < getScreen().getWidth()) {
+ setX((getScreen().getWidth() - getWidth()) / 2);
+ } else {
+ setX(0);
+ }
+ setY(((application.getDesktopBottom()
+ - application.getDesktopTop()) - getHeight()) / 2);
+ if (getY() < 0) {
+ setY(0);
+ }
+ setY(getY() + application.getDesktopTop());
+ }
+ }
+
+ /**
+ * Maximize window.
+ */
+ public void maximize() {
+ if (maximized) {
+ return;
+ }
+
+ restoreWindowWidth = getWidth();
+ restoreWindowHeight = getHeight();
+ restoreWindowX = getX();
+ restoreWindowY = getY();
+ setWidth(getScreen().getWidth());
+ setHeight(application.getDesktopBottom() - application.getDesktopTop());
+ setX(0);
+ setY(application.getDesktopTop());
+ maximized = true;
+
+ onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
+ getHeight()));
+ }
+
+ /**
+ * Restore (unmaximize) window.
+ */
+ public void restore() {
+ if (!maximized) {
+ return;
+ }
+
+ setWidth(restoreWindowWidth);
+ setHeight(restoreWindowHeight);
+ setX(restoreWindowX);
+ setY(restoreWindowY);
+ maximized = false;
+
+ onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
+ getHeight()));
+ }
+
+ /**
+ * Returns true if this window is hidden.
+ *
+ * @return true if this window is hidden, false if the window is shown
+ */
+ public final boolean isHidden() {
+ return hidden;
+ }
+
+ /**
+ * Returns true if this window is shown.
+ *
+ * @return true if this window is shown, false if the window is hidden
+ */
+ public final boolean isShown() {
+ return !hidden;
+ }
+
+ /**
+ * Hide window. A hidden window will still have its onIdle() called, and
+ * will also have onClose() called at application exit. Hidden windows
+ * will not receive any other events.
+ */
+ public void hide() {
+ application.hideWindow(this);
+ }
+
+ /**
+ * Show window.
+ */
+ public void show() {
+ application.showWindow(this);
+ }
+
+ /**
+ * Activate window (bring to top and receive events).
+ */
+ @Override
+ public void activate() {
+ application.activateWindow(this);
+ }
+
+ /**
+ * Close window. Note that windows without a close box can still be
+ * closed by calling the close() method.
+ */
+ @Override
+ public void close() {
+ application.closeWindow(this);
+ }
+
+ /**
+ * See if this window is undergoing any movement/resize/etc.
+ *
+ * @return true if the window is moving
+ */
+ public boolean inMovements() {
+ if (inWindowResize || inWindowMove || inKeyboardResize) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Stop any pending movement/resize/etc.
+ */
+ public void stopMovements() {
+ inWindowResize = false;
+ inWindowMove = false;
+ inKeyboardResize = false;
+ }
+
+ /**
+ * Returns true if this window is modal.
+ *
+ * @return true if this window is modal
+ */
+ public final boolean isModal() {
+ if ((flags & MODAL) == 0) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Returns true if this window has a close box.
+ *
+ * @return true if this window has a close box
+ */
+ public final boolean hasCloseBox() {
+ if ((flags & NOCLOSEBOX) != 0) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if this window has a maximize/zoom box.
+ *
+ * @return true if this window has a maximize/zoom box
+ */
+ public final boolean hasZoomBox() {
+ if ((flags & NOZOOMBOX) != 0) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if this window does not want menus to work while it is
+ * visible.
+ *
+ * @return true if this window does not want menus to work while it is
+ * visible
+ */
+ public final boolean hasOverriddenMenu() {
+ if ((flags & OVERRIDEMENU) != 0) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Retrieve the background color.
+ *
+ * @return the background color
+ */
+ public CellAttributes getBackground() {
+ if (!isModal()
+ && (inWindowMove || inWindowResize || inKeyboardResize)
+ ) {
+ assert (isActive());
+ return getTheme().getColor("twindow.background.windowmove");
+ } else if (isModal() && inWindowMove) {
+ assert (isActive());
+ return getTheme().getColor("twindow.background.modal");
+ } else if (isModal()) {
+ if (isActive()) {
+ return getTheme().getColor("twindow.background.modal");
+ }
+ return getTheme().getColor("twindow.background.modal.inactive");
+ } else if (isActive()) {
+ assert (!isModal());
+ return getTheme().getColor("twindow.background");
+ } else {
+ assert (!isModal());
+ return getTheme().getColor("twindow.background.inactive");
+ }
+ }
+
+ /**
+ * Retrieve the border color.
+ *
+ * @return the border color
+ */
+ public CellAttributes getBorder() {
+ if (!isModal()
+ && (inWindowMove || inWindowResize || inKeyboardResize)
+ ) {
+ if (!isActive()) {
+ // The user's terminal never passed a mouse up event, and now
+ // another window is active but we never finished a drag.
+ inWindowMove = false;
+ inWindowResize = false;
+ inKeyboardResize = false;
+ return getTheme().getColor("twindow.border.inactive");
+ }
+
+ return getTheme().getColor("twindow.border.windowmove");
+ } else if (isModal() && inWindowMove) {
+ assert (isActive());
+ return getTheme().getColor("twindow.border.modal.windowmove");
+ } else if (isModal()) {
+ if (isActive()) {
+ return getTheme().getColor("twindow.border.modal");
+ } else {
+ return getTheme().getColor("twindow.border.modal.inactive");
+ }
+ } else if (isActive()) {
+ assert (!isModal());
+ return getTheme().getColor("twindow.border");
+ } else {
+ assert (!isModal());
+ return getTheme().getColor("twindow.border.inactive");
+ }
+ }
+
+ /**
+ * Retrieve the color used by the window movement/sizing controls.
+ *
+ * @return the color used by the zoom box, resize bar, and close box
+ */
+ public CellAttributes getBorderControls() {
+ if (isModal()) {
+ return getTheme().getColor("twindow.border.modal.windowmove");
+ }
+ return getTheme().getColor("twindow.border.windowmove");
+ }
+
+ /**
+ * Retrieve the border line type.
+ *
+ * @return the border line type
+ */
+ private int getBorderType() {
+ if (!isModal()
+ && (inWindowMove || inWindowResize || inKeyboardResize)
+ ) {
+ assert (isActive());
+ return 1;
+ } else if (isModal() && inWindowMove) {
+ assert (isActive());
+ return 1;
+ } else if (isModal()) {
+ if (isActive()) {
+ return 2;
+ } else {
+ return 1;
+ }
+ } else if (isActive()) {
+ return 2;
+ } else {
+ return 1;
+ }
+ }
+
+ /**
+ * Returns true if this window does not want the application-wide mouse
+ * cursor drawn over it.
+ *
+ * @return true if this window does not want the application-wide mouse
+ * cursor drawn over it
+ */
+ public boolean hasHiddenMouse() {
+ return hideMouse;
+ }
+
+ /**
+ * Set request to prevent the application-wide mouse cursor from being
+ * drawn over this window.
+ *
+ * @param hideMouse if true, this window does not want the
+ * application-wide mouse cursor drawn over it
+ */
+ public final void setHiddenMouse(final boolean hideMouse) {
+ this.hideMouse = hideMouse;
+ }
+
+ /**
+ * Generate a human-readable string for this window.
+ *
+ * @return a human-readable string
+ */
+ @Override
+ public String toString() {
+ return String.format("%s(%8x) \'%s\' position (%d, %d) geometry %dx%d" +
+ " hidden %s modal %s", getClass().getName(), hashCode(), title,
+ getX(), getY(), getWidth(), getHeight(), hidden, isModal());
+ }
+
+ }
--- /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;
+
+ /**
+ * This interface provides a screen, keyboard, and mouse to TApplication. It
+ * also exposes session information as gleaned from lower levels of the
+ * communication stack.
+ */
+ public interface Backend {
+
+ /**
+ * Get a SessionInfo, which exposes text width/height, language,
+ * username, and other information from the communication stack.
+ *
+ * @return the SessionInfo
+ */
+ public SessionInfo getSessionInfo();
+
+ /**
+ * Get a Screen, which displays the text cells to the user.
+ *
+ * @return the Screen
+ */
+ public Screen getScreen();
+
+ /**
+ * Classes must provide an implementation that syncs the logical screen
+ * to the physical device.
+ */
+ public void flushScreen();
+
+ /**
+ * Check if there are events in the queue.
+ *
+ * @return if true, getEvents() has something to return to the application
+ */
+ public boolean hasEvents();
+
+ /**
+ * Classes 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);
+
+ /**
+ * Classes must provide an implementation that closes sockets, restores
+ * console, etc.
+ */
+ public void shutdown();
+
+ /**
+ * Classes must provide an implementation that sets the window title.
+ *
+ * @param title the new title
+ */
+ public void setTitle(final String title);
+
+ /**
+ * 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);
+
+ /**
+ * Reload backend options from System properties.
+ */
+ public void reloadOptions();
+
+ }
--- /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.io.InputStream;
+ import java.io.OutputStream;
+ import java.io.PrintWriter;
+ import java.io.Reader;
+ import java.io.UnsupportedEncodingException;
+
+ /**
+ * This class uses an xterm/ANSI X3.64/ECMA-48 type terminal to provide a
+ * screen, keyboard, and mouse to TApplication.
+ */
+ public class ECMA48Backend extends GenericBackend {
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor will use System.in and System.out and UTF-8
+ * encoding. On non-Windows systems System.in will be put in raw mode;
+ * shutdown() will (blindly!) put System.in in cooked mode.
+ *
+ * @throws UnsupportedEncodingException if an exception is thrown when
+ * creating the InputStreamReader
+ */
+ public ECMA48Backend() throws UnsupportedEncodingException {
+ this(null, null, null);
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param listener the object this backend needs to wake up when new
+ * input comes in
+ * @param input an InputStream connected to the remote user, or null for
+ * System.in. If System.in is used, then on non-Windows systems it will
+ * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
+ * mode. input is always converted to a Reader with UTF-8 encoding.
+ * @param output an OutputStream connected to the remote user, or null
+ * for System.out. output is always converted to a Writer with UTF-8
+ * encoding.
+ * @param windowWidth the number of text columns to start with
+ * @param windowHeight the number of text rows to start with
+ * @param fontSize the size in points. ECMA48 cannot set it, but it is
+ * here to match the Swing API.
+ * @throws UnsupportedEncodingException if an exception is thrown when
+ * creating the InputStreamReader
+ */
+ public ECMA48Backend(final Object listener, final InputStream input,
+ final OutputStream output, final int windowWidth,
+ final int windowHeight, final int fontSize)
+ throws UnsupportedEncodingException {
+
+ // Create a terminal and explicitly set stdin into raw mode
+ terminal = new ECMA48Terminal(listener, input, output, windowWidth,
+ windowHeight);
+
+ // Keep the terminal's sessionInfo so that TApplication can see it
+ sessionInfo = ((ECMA48Terminal) terminal).getSessionInfo();
+
+ // ECMA48Terminal is the screen too
+ screen = (ECMA48Terminal) terminal;
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param listener the object this backend needs to wake up when new
+ * input comes in
+ * @param input an InputStream connected to the remote user, or null for
+ * System.in. If System.in is used, then on non-Windows systems it will
+ * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
+ * mode. input is always converted to a Reader with UTF-8 encoding.
+ * @param output an OutputStream connected to the remote user, or null
+ * for System.out. output is always converted to a Writer with UTF-8
+ * encoding.
+ * @throws UnsupportedEncodingException if an exception is thrown when
+ * creating the InputStreamReader
+ */
+ public ECMA48Backend(final Object listener, final InputStream input,
+ final OutputStream output) throws UnsupportedEncodingException {
+
+ // Create a terminal and explicitly set stdin into raw mode
+ terminal = new ECMA48Terminal(listener, input, output);
+
+ // Keep the terminal's sessionInfo so that TApplication can see it
+ sessionInfo = ((ECMA48Terminal) terminal).getSessionInfo();
+
+ // ECMA48Terminal is the screen too
+ screen = (ECMA48Terminal) terminal;
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param listener the object this backend needs to wake up when new
+ * input comes in
+ * @param input the InputStream underlying 'reader'. Its available()
+ * method is used to determine if reader.read() will block or not.
+ * @param reader a Reader connected to the remote user.
+ * @param writer a PrintWriter connected to the remote user.
+ * @param setRawMode if true, set System.in into raw mode with stty.
+ * This should in general not be used. It is here solely for Demo3,
+ * which uses System.in.
+ * @throws IllegalArgumentException if input, reader, or writer are null.
+ */
+ public ECMA48Backend(final Object listener, final InputStream input,
+ final Reader reader, final PrintWriter writer,
+ final boolean setRawMode) {
+
+ // Create a terminal and explicitly set stdin into raw mode
+ terminal = new ECMA48Terminal(listener, input, reader, writer,
+ setRawMode);
+
+ // Keep the terminal's sessionInfo so that TApplication can see it
+ sessionInfo = ((ECMA48Terminal) terminal).getSessionInfo();
+
+ // ECMA48Terminal is the screen too
+ screen = (ECMA48Terminal) terminal;
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param listener the object this backend needs to wake up when new
+ * input comes in
+ * @param input the InputStream underlying 'reader'. Its available()
+ * method is used to determine if reader.read() will block or not.
+ * @param reader a Reader connected to the remote user.
+ * @param writer a PrintWriter connected to the remote user.
+ * @throws IllegalArgumentException if input, reader, or writer are null.
+ */
+ public ECMA48Backend(final Object listener, final InputStream input,
+ final Reader reader, final PrintWriter writer) {
+
+ this(listener, input, reader, writer, false);
+ }
+
+ }
--- /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.awt.image.BufferedImage;
+ import java.io.BufferedReader;
+ import java.io.ByteArrayOutputStream;
+ import java.io.FileDescriptor;
+ import java.io.FileInputStream;
+ import java.io.InputStream;
+ import java.io.InputStreamReader;
+ import java.io.IOException;
+ import java.io.OutputStream;
+ import java.io.OutputStreamWriter;
+ import java.io.PrintWriter;
+ import java.io.Reader;
+ import java.io.UnsupportedEncodingException;
+ import java.util.ArrayList;
+ import java.util.Collections;
+ import java.util.HashMap;
+ import java.util.List;
+ import javax.imageio.ImageIO;
+
+ import jexer.TImage;
+ import jexer.bits.Cell;
+ import jexer.bits.CellAttributes;
+ import jexer.bits.Color;
+ import jexer.event.TCommandEvent;
+ import jexer.event.TInputEvent;
+ import jexer.event.TKeypressEvent;
+ import jexer.event.TMouseEvent;
+ import jexer.event.TResizeEvent;
+ import static jexer.TCommand.*;
+ import static jexer.TKeypress.*;
+
+ /**
+ * This class reads keystrokes and mouse events and emits output to ANSI
+ * X3.64 / ECMA-48 type terminals e.g. xterm, linux, vt100, ansi.sys, etc.
+ */
+ public class ECMA48Terminal extends LogicalScreen
+ implements TerminalReader, Runnable {
+
+ // ------------------------------------------------------------------------
+ // Constants --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * States in the input parser.
+ */
+ private enum ParseState {
+ GROUND,
+ ESCAPE,
+ ESCAPE_INTERMEDIATE,
+ CSI_ENTRY,
+ CSI_PARAM,
+ MOUSE,
+ MOUSE_SGR,
+ }
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Emit debugging to stderr.
+ */
+ private boolean debugToStderr = false;
+
+ /**
+ * If true, emit T.416-style RGB colors for normal system colors. This
+ * is a) expensive in bandwidth, and b) potentially terrible looking for
+ * non-xterms.
+ */
+ private static boolean doRgbColor = false;
+
+ /**
+ * The session information.
+ */
+ private SessionInfo sessionInfo;
+
+ /**
+ * The event queue, filled up by a thread reading on input.
+ */
+ private List<TInputEvent> eventQueue;
+
+ /**
+ * If true, we want the reader thread to exit gracefully.
+ */
+ private boolean stopReaderThread;
+
+ /**
+ * The reader thread.
+ */
+ private Thread readerThread;
+
+ /**
+ * Parameters being collected. E.g. if the string is \033[1;3m, then
+ * params[0] will be 1 and params[1] will be 3.
+ */
+ private List<String> params;
+
+ /**
+ * Current parsing state.
+ */
+ private ParseState state;
+
+ /**
+ * The time we entered ESCAPE. If we get a bare escape without a code
+ * following it, this is used to return that bare escape.
+ */
+ private long escapeTime;
+
+ /**
+ * The time we last checked the window size. We try not to spawn stty
+ * more than once per second.
+ */
+ private long windowSizeTime;
+
+ /**
+ * true if mouse1 was down. Used to report mouse1 on the release event.
+ */
+ private boolean mouse1;
+
+ /**
+ * true if mouse2 was down. Used to report mouse2 on the release event.
+ */
+ private boolean mouse2;
+
+ /**
+ * true if mouse3 was down. Used to report mouse3 on the release event.
+ */
+ private boolean mouse3;
+
+ /**
+ * Cache the cursor visibility value so we only emit the sequence when we
+ * need to.
+ */
+ private boolean cursorOn = true;
+
+ /**
+ * Cache the last window size to figure out if a TResizeEvent needs to be
+ * generated.
+ */
+ private TResizeEvent windowResize = null;
+
+ /**
+ * If true, emit wide-char (CJK/Emoji) characters as sixel images.
+ */
+ private boolean wideCharImages = true;
+
+ /**
+ * Window width in pixels. Used for sixel support.
+ */
+ private int widthPixels = 640;
+
+ /**
+ * Window height in pixels. Used for sixel support.
+ */
+ private int heightPixels = 400;
+
+ /**
+ * If true, emit image data via sixel.
+ */
+ private boolean sixel = true;
+
+ /**
+ * The sixel palette handler.
+ */
+ private SixelPalette palette = null;
+
+ /**
+ * The sixel post-rendered string cache.
+ */
+ private ImageCache sixelCache = null;
+
+ /**
+ * Number of colors in the sixel palette. Xterm 335 defines the max as
+ * 1024. Valid values are: 2 (black and white), 256, 512, 1024, and
+ * 2048.
+ */
+ private int sixelPaletteSize = 1024;
+
+ /**
+ * If true, emit image data via iTerm2 image protocol.
+ */
+ private boolean iterm2Images = false;
+
+ /**
+ * The iTerm2 post-rendered string cache.
+ */
+ private ImageCache iterm2Cache = null;
+
+ /**
+ * If true, emit image data via Jexer image protocol.
+ */
+ private boolean jexerImages = false;
+
+ /**
+ * 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.
+ */
+ private boolean setRawMode = false;
+
+ /**
+ * If true, '?' was seen in terminal response.
+ */
+ private boolean decPrivateModeFlag = false;
+
+ /**
+ * The terminal's input. If an InputStream is not specified in the
+ * constructor, then this InputStreamReader will be bound to System.in
+ * with UTF-8 encoding.
+ */
+ private Reader input;
+
+ /**
+ * The terminal's raw InputStream. If an InputStream is not specified in
+ * the constructor, then this InputReader will be bound to System.in.
+ * This is used by run() to see if bytes are available() before calling
+ * (Reader)input.read().
+ */
+ private InputStream inputStream;
+
+ /**
+ * The terminal's output. If an OutputStream is not specified in the
+ * constructor, then this PrintWriter will be bound to System.out with
+ * UTF-8 encoding.
+ */
+ private PrintWriter output;
+
+ /**
+ * The listening object that run() wakes up on new input.
+ */
+ private Object listener;
+
+ // Colors to map DOS colors to AWT colors.
+ private static java.awt.Color MYBLACK;
+ private static java.awt.Color MYRED;
+ private static java.awt.Color MYGREEN;
+ private static java.awt.Color MYYELLOW;
+ private static java.awt.Color MYBLUE;
+ private static java.awt.Color MYMAGENTA;
+ private static java.awt.Color MYCYAN;
+ private static java.awt.Color MYWHITE;
+ private static java.awt.Color MYBOLD_BLACK;
+ private static java.awt.Color MYBOLD_RED;
+ private static java.awt.Color MYBOLD_GREEN;
+ private static java.awt.Color MYBOLD_YELLOW;
+ private static java.awt.Color MYBOLD_BLUE;
+ private static java.awt.Color MYBOLD_MAGENTA;
+ private static java.awt.Color MYBOLD_CYAN;
+ private static java.awt.Color MYBOLD_WHITE;
+
+ /**
+ * SixelPalette is used to manage the conversion of images between 24-bit
+ * RGB color and a palette of sixelPaletteSize colors.
+ */
+ private class SixelPalette {
+
+ /**
+ * Color palette for sixel output, sorted low to high.
+ */
+ private List<Integer> rgbColors = new ArrayList<Integer>();
+
+ /**
+ * Map of color palette index for sixel output, from the order it was
+ * generated by makePalette() to rgbColors.
+ */
+ private int [] rgbSortedIndex = new int[sixelPaletteSize];
+
+ /**
+ * The color palette, organized by hue, saturation, and luminance.
+ * This is used for a fast color match.
+ */
+ private ArrayList<ArrayList<ArrayList<ColorIdx>>> hslColors;
+
+ /**
+ * Number of bits for hue.
+ */
+ private int hueBits = -1;
+
+ /**
+ * Number of bits for saturation.
+ */
+ private int satBits = -1;
+
+ /**
+ * Number of bits for luminance.
+ */
+ private int lumBits = -1;
+
+ /**
+ * Step size for hue bins.
+ */
+ private int hueStep = -1;
+
+ /**
+ * Step size for saturation bins.
+ */
+ private int satStep = -1;
+
+ /**
+ * Cached RGB to HSL result.
+ */
+ private int hsl[] = new int[3];
+
+ /**
+ * ColorIdx records a RGB color and its palette index.
+ */
+ private class ColorIdx {
+ /**
+ * The 24-bit RGB color.
+ */
+ public int color;
+
+ /**
+ * The palette index for this color.
+ */
+ public int index;
+
+ /**
+ * Public constructor.
+ *
+ * @param color the 24-bit RGB color
+ * @param index the palette index for this color
+ */
+ public ColorIdx(final int color, final int index) {
+ this.color = color;
+ this.index = index;
+ }
+ }
+
+ /**
+ * Public constructor.
+ */
+ public SixelPalette() {
+ makePalette();
+ }
+
+ /**
+ * Find the nearest match for a color in the palette.
+ *
+ * @param color the RGB color
+ * @return the index in rgbColors that is closest to color
+ */
+ public int matchColor(final int color) {
+
+ assert (color >= 0);
+
+ /*
+ * matchColor() is a critical performance bottleneck. To make it
+ * decent, we do the following:
+ *
+ * 1. Find the nearest two hues that bracket this color.
+ *
+ * 2. Find the nearest two saturations that bracket this color.
+ *
+ * 3. Iterate within these four bands of luminance values,
+ * returning the closest color by Euclidean distance.
+ *
+ * This strategy reduces the search space by about 97%.
+ */
+ int red = (color >>> 16) & 0xFF;
+ int green = (color >>> 8) & 0xFF;
+ int blue = color & 0xFF;
+
+ if (sixelPaletteSize == 2) {
+ if (((red * red) + (green * green) + (blue * blue)) < 35568) {
+ // Black
+ return 0;
+ }
+ // White
+ return 1;
+ }
+
+
+ rgbToHsl(red, green, blue, hsl);
+ int hue = hsl[0];
+ int sat = hsl[1];
+ int lum = hsl[2];
+ // System.err.printf("%d %d %d\n", hue, sat, lum);
+
+ double diff = Double.MAX_VALUE;
+ int idx = -1;
+
+ int hue1 = hue / (360/hueStep);
+ int hue2 = hue1 + 1;
+ if (hue1 >= hslColors.size() - 1) {
+ // Bracket pure red from above.
+ hue1 = hslColors.size() - 1;
+ hue2 = 0;
+ } else if (hue1 == 0) {
+ // Bracket pure red from below.
+ hue2 = hslColors.size() - 1;
+ }
+
+ for (int hI = hue1; hI != -1;) {
+ ArrayList<ArrayList<ColorIdx>> sats = hslColors.get(hI);
+ if (hI == hue1) {
+ hI = hue2;
+ } else if (hI == hue2) {
+ hI = -1;
+ }
+
+ int sMin = (sat / satStep) - 1;
+ int sMax = sMin + 1;
+ if (sMin < 0) {
+ sMin = 0;
+ sMax = 1;
+ } else if (sMin == sats.size() - 1) {
+ sMax = sMin;
+ sMin--;
+ }
+ assert (sMin >= 0);
+ assert (sMax - sMin == 1);
+
+ // int sMin = 0;
+ // int sMax = sats.size() - 1;
+
+ for (int sI = sMin; sI <= sMax; sI++) {
+ ArrayList<ColorIdx> lums = sats.get(sI);
+
+ // True 3D colorspace match for the remaining values
+ for (ColorIdx c: lums) {
+ int rgbColor = c.color;
+ double newDiff = 0;
+ int red2 = (rgbColor >>> 16) & 0xFF;
+ int green2 = (rgbColor >>> 8) & 0xFF;
+ int blue2 = rgbColor & 0xFF;
+ newDiff += Math.pow(red2 - red, 2);
+ newDiff += Math.pow(green2 - green, 2);
+ newDiff += Math.pow(blue2 - blue, 2);
+ if (newDiff < diff) {
+ idx = rgbSortedIndex[c.index];
+ diff = newDiff;
+ }
+ }
+ }
+ }
+
+ if (((red * red) + (green * green) + (blue * blue)) < diff) {
+ // Black is a closer match.
+ idx = 0;
+ } else if ((((255 - red) * (255 - red)) +
+ ((255 - green) * (255 - green)) +
+ ((255 - blue) * (255 - blue))) < diff) {
+
+ // White is a closer match.
+ idx = sixelPaletteSize - 1;
+ }
+ assert (idx != -1);
+ return idx;
+ }
+
+ /**
+ * Clamp an int value to [0, 255].
+ *
+ * @param x the int value
+ * @return an int between 0 and 255.
+ */
+ private int clamp(final int x) {
+ if (x < 0) {
+ return 0;
+ }
+ if (x > 255) {
+ return 255;
+ }
+ return x;
+ }
+
+ /**
+ * Dither an image to a sixelPaletteSize palette. The dithered
+ * image cells will contain indexes into the palette.
+ *
+ * @param image the image to dither
+ * @return the dithered image. Every pixel is an index into the
+ * palette.
+ */
+ public BufferedImage ditherImage(final BufferedImage image) {
+
+ BufferedImage ditheredImage = new BufferedImage(image.getWidth(),
+ image.getHeight(), BufferedImage.TYPE_INT_ARGB);
+
+ int [] rgbArray = image.getRGB(0, 0, image.getWidth(),
+ image.getHeight(), null, 0, image.getWidth());
+ ditheredImage.setRGB(0, 0, image.getWidth(), image.getHeight(),
+ rgbArray, 0, image.getWidth());
+
+ for (int imageY = 0; imageY < image.getHeight(); imageY++) {
+ for (int imageX = 0; imageX < image.getWidth(); imageX++) {
+ int oldPixel = ditheredImage.getRGB(imageX,
+ imageY) & 0xFFFFFF;
+ int colorIdx = matchColor(oldPixel);
+ assert (colorIdx >= 0);
+ assert (colorIdx < sixelPaletteSize);
+ int newPixel = rgbColors.get(colorIdx);
+ ditheredImage.setRGB(imageX, imageY, colorIdx);
+
+ int oldRed = (oldPixel >>> 16) & 0xFF;
+ int oldGreen = (oldPixel >>> 8) & 0xFF;
+ int oldBlue = oldPixel & 0xFF;
+
+ int newRed = (newPixel >>> 16) & 0xFF;
+ int newGreen = (newPixel >>> 8) & 0xFF;
+ int newBlue = newPixel & 0xFF;
+
+ int redError = (oldRed - newRed) / 16;
+ int greenError = (oldGreen - newGreen) / 16;
+ int blueError = (oldBlue - newBlue) / 16;
+
+ int red, green, blue;
+ if (imageX < image.getWidth() - 1) {
+ int pXpY = ditheredImage.getRGB(imageX + 1, imageY);
+ red = ((pXpY >>> 16) & 0xFF) + (7 * redError);
+ green = ((pXpY >>> 8) & 0xFF) + (7 * greenError);
+ blue = ( pXpY & 0xFF) + (7 * blueError);
+ red = clamp(red);
+ green = clamp(green);
+ blue = clamp(blue);
+ pXpY = ((red & 0xFF) << 16);
+ pXpY |= ((green & 0xFF) << 8) | (blue & 0xFF);
+ ditheredImage.setRGB(imageX + 1, imageY, pXpY);
+
+ if (imageY < image.getHeight() - 1) {
+ int pXpYp = ditheredImage.getRGB(imageX + 1,
+ imageY + 1);
+ red = ((pXpYp >>> 16) & 0xFF) + redError;
+ green = ((pXpYp >>> 8) & 0xFF) + greenError;
+ blue = ( pXpYp & 0xFF) + blueError;
+ red = clamp(red);
+ green = clamp(green);
+ blue = clamp(blue);
+ pXpYp = ((red & 0xFF) << 16);
+ pXpYp |= ((green & 0xFF) << 8) | (blue & 0xFF);
+ ditheredImage.setRGB(imageX + 1, imageY + 1, pXpYp);
+ }
+ } else if (imageY < image.getHeight() - 1) {
+ int pXmYp = ditheredImage.getRGB(imageX - 1,
+ imageY + 1);
+ int pXYp = ditheredImage.getRGB(imageX,
+ imageY + 1);
+
+ red = ((pXmYp >>> 16) & 0xFF) + (3 * redError);
+ green = ((pXmYp >>> 8) & 0xFF) + (3 * greenError);
+ blue = ( pXmYp & 0xFF) + (3 * blueError);
+ red = clamp(red);
+ green = clamp(green);
+ blue = clamp(blue);
+ pXmYp = ((red & 0xFF) << 16);
+ pXmYp |= ((green & 0xFF) << 8) | (blue & 0xFF);
+ ditheredImage.setRGB(imageX - 1, imageY + 1, pXmYp);
+
+ red = ((pXYp >>> 16) & 0xFF) + (5 * redError);
+ green = ((pXYp >>> 8) & 0xFF) + (5 * greenError);
+ blue = ( pXYp & 0xFF) + (5 * blueError);
+ red = clamp(red);
+ green = clamp(green);
+ blue = clamp(blue);
+ pXYp = ((red & 0xFF) << 16);
+ pXYp |= ((green & 0xFF) << 8) | (blue & 0xFF);
+ ditheredImage.setRGB(imageX, imageY + 1, pXYp);
+ }
+ } // for (int imageY = 0; imageY < image.getHeight(); imageY++)
+ } // for (int imageX = 0; imageX < image.getWidth(); imageX++)
+
+ return ditheredImage;
+ }
+
+ /**
+ * Convert an RGB color to HSL.
+ *
+ * @param red red color, between 0 and 255
+ * @param green green color, between 0 and 255
+ * @param blue blue color, between 0 and 255
+ * @param hsl the hsl color as [hue, saturation, luminance]
+ */
+ private void rgbToHsl(final int red, final int green,
+ final int blue, final int [] hsl) {
+
+ assert ((red >= 0) && (red <= 255));
+ assert ((green >= 0) && (green <= 255));
+ assert ((blue >= 0) && (blue <= 255));
+
+ double R = red / 255.0;
+ double G = green / 255.0;
+ double B = blue / 255.0;
+ boolean Rmax = false;
+ boolean Gmax = false;
+ boolean Bmax = false;
+ double min = (R < G ? R : G);
+ min = (min < B ? min : B);
+ double max = 0;
+ if ((R >= G) && (R >= B)) {
+ max = R;
+ Rmax = true;
+ } else if ((G >= R) && (G >= B)) {
+ max = G;
+ Gmax = true;
+ } else if ((B >= G) && (B >= R)) {
+ max = B;
+ Bmax = true;
+ }
+
+ double L = (min + max) / 2.0;
+ double H = 0.0;
+ double S = 0.0;
+ if (min != max) {
+ if (L < 0.5) {
+ S = (max - min) / (max + min);
+ } else {
+ S = (max - min) / (2.0 - max - min);
+ }
+ }
+ if (Rmax) {
+ assert (Gmax == false);
+ assert (Bmax == false);
+ H = (G - B) / (max - min);
+ } else if (Gmax) {
+ assert (Rmax == false);
+ assert (Bmax == false);
+ H = 2.0 + (B - R) / (max - min);
+ } else if (Bmax) {
+ assert (Rmax == false);
+ assert (Gmax == false);
+ H = 4.0 + (R - G) / (max - min);
+ }
+ if (H < 0.0) {
+ H += 6.0;
+ }
+ hsl[0] = (int) (H * 60.0);
+ hsl[1] = (int) (S * 100.0);
+ hsl[2] = (int) (L * 100.0);
+
+ assert ((hsl[0] >= 0) && (hsl[0] <= 360));
+ assert ((hsl[1] >= 0) && (hsl[1] <= 100));
+ assert ((hsl[2] >= 0) && (hsl[2] <= 100));
+ }
+
+ /**
+ * Convert a HSL color to RGB.
+ *
+ * @param hue hue, between 0 and 359
+ * @param sat saturation, between 0 and 100
+ * @param lum luminance, between 0 and 100
+ * @return the rgb color as 0x00RRGGBB
+ */
+ private int hslToRgb(final int hue, final int sat, final int lum) {
+ assert ((hue >= 0) && (hue <= 360));
+ assert ((sat >= 0) && (sat <= 100));
+ assert ((lum >= 0) && (lum <= 100));
+
+ double S = sat / 100.0;
+ double L = lum / 100.0;
+ double C = (1.0 - Math.abs((2.0 * L) - 1.0)) * S;
+ double Hp = hue / 60.0;
+ double X = C * (1.0 - Math.abs((Hp % 2) - 1.0));
+ double Rp = 0.0;
+ double Gp = 0.0;
+ double Bp = 0.0;
+ if (Hp <= 1.0) {
+ Rp = C;
+ Gp = X;
+ } else if (Hp <= 2.0) {
+ Rp = X;
+ Gp = C;
+ } else if (Hp <= 3.0) {
+ Gp = C;
+ Bp = X;
+ } else if (Hp <= 4.0) {
+ Gp = X;
+ Bp = C;
+ } else if (Hp <= 5.0) {
+ Rp = X;
+ Bp = C;
+ } else if (Hp <= 6.0) {
+ Rp = C;
+ Bp = X;
+ }
+ double m = L - (C / 2.0);
+ int red = ((int) ((Rp + m) * 255.0)) << 16;
+ int green = ((int) ((Gp + m) * 255.0)) << 8;
+ int blue = (int) ((Bp + m) * 255.0);
+
+ return (red | green | blue);
+ }
+
+ /**
+ * Create the sixel palette.
+ */
+ private void makePalette() {
+ // Generate the sixel palette. Because we have no idea at this
+ // layer which image(s) will be shown, we have to use a common
+ // palette with sixelPaletteSize colors for everything, and
+ // map the BufferedImage colors to their nearest neighbor in RGB
+ // space.
+
+ if (sixelPaletteSize == 2) {
+ rgbColors.add(0);
+ rgbColors.add(0xFFFFFF);
+ rgbSortedIndex[0] = 0;
+ rgbSortedIndex[1] = 1;
+ return;
+ }
+
+ // We build a palette using the Hue-Saturation-Luminence model,
+ // with 5+ bits for Hue, 2+ bits for Saturation, and 1+ bit for
+ // Luminance. We convert these colors to 24-bit RGB, sort them
+ // ascending, and steal the first index for pure black and the
+ // last for pure white. The 8-bit final palette favors bright
+ // colors, somewhere between pastel and classic television
+ // technicolor. 9- and 10-bit palettes are more uniform.
+
+ // Default at 256 colors.
+ hueBits = 5;
+ satBits = 2;
+ lumBits = 1;
+
+ assert (sixelPaletteSize >= 256);
+ assert ((sixelPaletteSize == 256)
+ || (sixelPaletteSize == 512)
+ || (sixelPaletteSize == 1024)
+ || (sixelPaletteSize == 2048));
+
+ switch (sixelPaletteSize) {
+ case 512:
+ hueBits = 5;
+ satBits = 2;
+ lumBits = 2;
+ break;
+ case 1024:
+ hueBits = 5;
+ satBits = 2;
+ lumBits = 3;
+ break;
+ case 2048:
+ hueBits = 5;
+ satBits = 3;
+ lumBits = 3;
+ break;
+ }
+ hueStep = (int) (Math.pow(2, hueBits));
+ satStep = (int) (100 / Math.pow(2, satBits));
+ // 1 bit for luminance: 40 and 70.
+ int lumBegin = 40;
+ int lumStep = 30;
+ switch (lumBits) {
+ case 2:
+ // 2 bits: 20, 40, 60, 80
+ lumBegin = 20;
+ lumStep = 20;
+ break;
+ case 3:
+ // 3 bits: 8, 20, 32, 44, 56, 68, 80, 92
+ lumBegin = 8;
+ lumStep = 12;
+ break;
+ }
+
+ // System.err.printf("<html><body>\n");
+ // Hue is evenly spaced around the wheel.
+ hslColors = new ArrayList<ArrayList<ArrayList<ColorIdx>>>();
+
+ final boolean DEBUG = false;
+ ArrayList<Integer> rawRgbList = new ArrayList<Integer>();
+
+ for (int hue = 0; hue < (360 - (360 % hueStep));
+ hue += (360/hueStep)) {
+
+ ArrayList<ArrayList<ColorIdx>> satList = null;
+ satList = new ArrayList<ArrayList<ColorIdx>>();
+ hslColors.add(satList);
+
+ // Saturation is linearly spaced between pastel and pure.
+ for (int sat = satStep; sat <= 100; sat += satStep) {
+
+ ArrayList<ColorIdx> lumList = new ArrayList<ColorIdx>();
+ satList.add(lumList);
+
+ // Luminance brackets the pure color, but leaning toward
+ // lighter.
+ for (int lum = lumBegin; lum < 100; lum += lumStep) {
+ /*
+ System.err.printf("<font style = \"color:");
+ System.err.printf("hsl(%d, %d%%, %d%%)",
+ hue, sat, lum);
+ System.err.printf(";\">=</font>\n");
+ */
+ int rgbColor = hslToRgb(hue, sat, lum);
+ rgbColors.add(rgbColor);
+ ColorIdx colorIdx = new ColorIdx(rgbColor,
+ rgbColors.size() - 1);
+ lumList.add(colorIdx);
+
+ rawRgbList.add(rgbColor);
+ if (DEBUG) {
+ int red = (rgbColor >>> 16) & 0xFF;
+ int green = (rgbColor >>> 8) & 0xFF;
+ int blue = rgbColor & 0xFF;
+ int [] backToHsl = new int[3];
+ rgbToHsl(red, green, blue, backToHsl);
+ System.err.printf("%d [%d] %d [%d] %d [%d]\n",
+ hue, backToHsl[0], sat, backToHsl[1],
+ lum, backToHsl[2]);
+ }
+ }
+ }
+ }
+ // System.err.printf("\n</body></html>\n");
+
+ assert (rgbColors.size() == sixelPaletteSize);
+
+ /*
+ * We need to sort rgbColors, so that toSixel() can know where
+ * BLACK and WHITE are in it. But we also need to be able to
+ * find the sorted values using the old unsorted indexes. So we
+ * will sort it, put all the indexes into a HashMap, and then
+ * build rgbSortedIndex[].
+ */
+ Collections.sort(rgbColors);
+ HashMap<Integer, Integer> rgbColorIndices = null;
+ rgbColorIndices = new HashMap<Integer, Integer>();
+ for (int i = 0; i < sixelPaletteSize; i++) {
+ rgbColorIndices.put(rgbColors.get(i), i);
+ }
+ for (int i = 0; i < sixelPaletteSize; i++) {
+ int rawColor = rawRgbList.get(i);
+ rgbSortedIndex[i] = rgbColorIndices.get(rawColor);
+ }
+ if (DEBUG) {
+ for (int i = 0; i < sixelPaletteSize; i++) {
+ assert (rawRgbList != null);
+ int idx = rgbSortedIndex[i];
+ int rgbColor = rgbColors.get(idx);
+ if ((idx != 0) && (idx != sixelPaletteSize - 1)) {
+ /*
+ System.err.printf("%d %06x --> %d %06x\n",
+ i, rawRgbList.get(i), idx, rgbColors.get(idx));
+ */
+ assert (rgbColor == rawRgbList.get(i));
+ }
+ }
+ }
+
+ // Set the dimmest color as true black, and the brightest as true
+ // white.
+ rgbColors.set(0, 0);
+ rgbColors.set(sixelPaletteSize - 1, 0xFFFFFF);
+
+ /*
+ System.err.printf("<html><body>\n");
+ for (Integer rgb: rgbColors) {
+ System.err.printf("<font style = \"color:");
+ System.err.printf("#%06x", rgb);
+ System.err.printf(";\">=</font>\n");
+ }
+ System.err.printf("\n</body></html>\n");
+ */
+
+ }
+
+ /**
+ * Emit the sixel palette.
+ *
+ * @param sb the StringBuilder to append to
+ * @param used array of booleans set to true for each color actually
+ * used in this cell, or null to emit the entire palette
+ * @return the string to emit to an ANSI / ECMA-style terminal
+ */
+ public String emitPalette(final StringBuilder sb,
+ final boolean [] used) {
+
+ for (int i = 0; i < sixelPaletteSize; i++) {
+ if (((used != null) && (used[i] == true)) || (used == null)) {
+ int rgbColor = rgbColors.get(i);
+ sb.append(String.format("#%d;2;%d;%d;%d", i,
+ ((rgbColor >>> 16) & 0xFF) * 100 / 255,
+ ((rgbColor >>> 8) & 0xFF) * 100 / 255,
+ ( rgbColor & 0xFF) * 100 / 255));
+ }
+ }
+ return sb.toString();
+ }
+ }
+
+ /**
+ * ImageCache is a least-recently-used cache that hangs on to the
+ * post-rendered sixel or iTerm2 string for a particular set of cells.
+ */
+ private class ImageCache {
+
+ /**
+ * Maximum size of the cache.
+ */
+ private int maxSize = 100;
+
+ /**
+ * The entries stored in the cache.
+ */
+ private HashMap<String, CacheEntry> cache = null;
+
+ /**
+ * CacheEntry is one entry in the cache.
+ */
+ private class CacheEntry {
+ /**
+ * The cache key.
+ */
+ public String key;
+
+ /**
+ * The cache data.
+ */
+ public String data;
+
+ /**
+ * The last time this entry was used.
+ */
+ public long millis = 0;
+
+ /**
+ * Public constructor.
+ *
+ * @param key the cache entry key
+ * @param data the cache entry data
+ */
+ public CacheEntry(final String key, final String data) {
+ this.key = key;
+ this.data = data;
+ this.millis = System.currentTimeMillis();
+ }
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param maxSize the maximum size of the cache
+ */
+ public ImageCache(final int maxSize) {
+ this.maxSize = maxSize;
+ cache = new HashMap<String, CacheEntry>();
+ }
+
+ /**
+ * Make a unique key for a list of cells.
+ *
+ * @param cells the cells
+ * @return the key
+ */
+ private String makeKey(final ArrayList<Cell> cells) {
+ StringBuilder sb = new StringBuilder();
+ for (Cell cell: cells) {
+ sb.append(cell.hashCode());
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Get an entry from the cache.
+ *
+ * @param cells the list of cells that are the cache key
+ * @return the sixel string representing these cells, or null if this
+ * list of cells is not in the cache
+ */
+ public String get(final ArrayList<Cell> cells) {
+ CacheEntry entry = cache.get(makeKey(cells));
+ if (entry == null) {
+ return null;
+ }
+ entry.millis = System.currentTimeMillis();
+ return entry.data;
+ }
+
+ /**
+ * Put an entry into the cache.
+ *
+ * @param cells the list of cells that are the cache key
+ * @param data the sixel string representing these cells
+ */
+ public void put(final ArrayList<Cell> cells, final String data) {
+ String key = makeKey(cells);
+
+ // System.err.println("put() " + key + " size " + cache.size());
+
+ assert (!cache.containsKey(key));
+
+ assert (cache.size() <= maxSize);
+ if (cache.size() == maxSize) {
+ // Cache is at limit, evict oldest entry.
+ long oldestTime = Long.MAX_VALUE;
+ String keyToRemove = null;
+ for (CacheEntry entry: cache.values()) {
+ if ((entry.millis < oldestTime) || (keyToRemove == null)) {
+ keyToRemove = entry.key;
+ oldestTime = entry.millis;
+ }
+ }
+ /*
+ System.err.println("put() remove key = " + keyToRemove +
+ " size " + cache.size());
+ */
+ assert (keyToRemove != null);
+ cache.remove(keyToRemove);
+ /*
+ System.err.println("put() removed, size " + cache.size());
+ */
+ }
+ assert (cache.size() <= maxSize);
+ CacheEntry entry = new CacheEntry(key, data);
+ assert (key.equals(entry.key));
+ cache.put(key, entry);
+ /*
+ System.err.println("put() added key " + key + " " +
+ " size " + cache.size());
+ */
+ }
+
+ }
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Constructor sets up state for getEvent(). If either windowWidth or
+ * windowHeight are less than 1, the terminal is not resized.
+ *
+ * @param listener the object this backend needs to wake up when new
+ * input comes in
+ * @param input an InputStream connected to the remote user, or null for
+ * System.in. If System.in is used, then on non-Windows systems it will
+ * be put in raw mode; closeTerminal() will (blindly!) put System.in in
+ * cooked mode. input is always converted to a Reader with UTF-8
+ * encoding.
+ * @param output an OutputStream connected to the remote user, or null
+ * for System.out. output is always converted to a Writer with UTF-8
+ * encoding.
+ * @param windowWidth the number of text columns to start with
+ * @param windowHeight the number of text rows to start with
+ * @throws UnsupportedEncodingException if an exception is thrown when
+ * creating the InputStreamReader
+ */
+ public ECMA48Terminal(final Object listener, final InputStream input,
+ final OutputStream output, final int windowWidth,
+ final int windowHeight) throws UnsupportedEncodingException {
+
+ this(listener, input, output);
+
+ // Send dtterm/xterm sequences, which will probably not work because
+ // allowWindowOps is defaulted to false.
+ if ((windowWidth > 0) && (windowHeight > 0)) {
+ String resizeString = String.format("\033[8;%d;%dt", windowHeight,
+ windowWidth);
+ this.output.write(resizeString);
+ this.output.flush();
+ }
+ }
+
+ /**
+ * Constructor sets up state for getEvent().
+ *
+ * @param listener the object this backend needs to wake up when new
+ * input comes in
+ * @param input an InputStream connected to the remote user, or null for
+ * System.in. If System.in is used, then on non-Windows systems it will
+ * be put in raw mode; closeTerminal() will (blindly!) put System.in in
+ * cooked mode. input is always converted to a Reader with UTF-8
+ * encoding.
+ * @param output an OutputStream connected to the remote user, or null
+ * for System.out. output is always converted to a Writer with UTF-8
+ * encoding.
+ * @throws UnsupportedEncodingException if an exception is thrown when
+ * creating the InputStreamReader
+ */
+ public ECMA48Terminal(final Object listener, final InputStream input,
+ final OutputStream output) throws UnsupportedEncodingException {
+
+ resetParser();
+ mouse1 = false;
+ mouse2 = false;
+ mouse3 = false;
+ stopReaderThread = false;
+ this.listener = listener;
+
+ if (input == null) {
+ // inputStream = System.in;
+ inputStream = new FileInputStream(FileDescriptor.in);
+ sttyRaw();
+ setRawMode = true;
+ } else {
+ inputStream = input;
+ }
+ this.input = new InputStreamReader(inputStream, "UTF-8");
+
+ if (input instanceof SessionInfo) {
+ // This is a TelnetInputStream that exposes window size and
+ // environment variables from the telnet layer.
+ sessionInfo = (SessionInfo) input;
+ }
+ if (sessionInfo == null) {
+ if (input == null) {
+ // Reading right off the tty
+ sessionInfo = new TTYSessionInfo();
+ } else {
+ sessionInfo = new TSessionInfo();
+ }
+ }
+
+ if (output == null) {
+ this.output = new PrintWriter(new OutputStreamWriter(System.out,
+ "UTF-8"));
+ } else {
+ this.output = new PrintWriter(new OutputStreamWriter(output,
+ "UTF-8"));
+ }
+
+ // Request Device Attributes
+ this.output.printf("\033[c");
+
+ // Request xterm report window/cell dimensions in pixels
+ this.output.printf("%s", xtermReportPixelDimensions());
+
+ // 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());
+
+ // Query the screen size
+ sessionInfo.queryWindowSize();
+ setDimensions(sessionInfo.getWindowWidth(),
+ sessionInfo.getWindowHeight());
+
+ // Hang onto the window size
+ windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN,
+ sessionInfo.getWindowWidth(), sessionInfo.getWindowHeight());
+
+ reloadOptions();
+
+ // Spin up the input reader
+ eventQueue = new ArrayList<TInputEvent>();
+ readerThread = new Thread(this);
+ readerThread.start();
+
+ // Clear the screen
+ this.output.write(clearAll());
+ this.output.flush();
+ }
+
+ /**
+ * Constructor sets up state for getEvent().
+ *
+ * @param listener the object this backend needs to wake up when new
+ * input comes in
+ * @param input the InputStream underlying 'reader'. Its available()
+ * method is used to determine if reader.read() will block or not.
+ * @param reader a Reader connected to the remote user.
+ * @param writer a PrintWriter connected to the remote user.
+ * @param setRawMode if true, set System.in into raw mode with stty.
+ * This should in general not be used. It is here solely for Demo3,
+ * which uses System.in.
+ * @throws IllegalArgumentException if input, reader, or writer are null.
+ */
+ public ECMA48Terminal(final Object listener, final InputStream input,
+ final Reader reader, final PrintWriter writer,
+ final boolean setRawMode) {
+
+ if (input == null) {
+ throw new IllegalArgumentException("InputStream must be specified");
+ }
+ if (reader == null) {
+ throw new IllegalArgumentException("Reader must be specified");
+ }
+ if (writer == null) {
+ throw new IllegalArgumentException("Writer must be specified");
+ }
+ resetParser();
+ mouse1 = false;
+ mouse2 = false;
+ mouse3 = false;
+ stopReaderThread = false;
+ this.listener = listener;
+
+ inputStream = input;
+ this.input = reader;
+
+ if (setRawMode == true) {
+ sttyRaw();
+ }
+ this.setRawMode = setRawMode;
+
+ if (input instanceof SessionInfo) {
+ // This is a TelnetInputStream that exposes window size and
+ // environment variables from the telnet layer.
+ sessionInfo = (SessionInfo) input;
+ }
+ if (sessionInfo == null) {
+ if (setRawMode == true) {
+ // Reading right off the tty
+ sessionInfo = new TTYSessionInfo();
+ } else {
+ sessionInfo = new TSessionInfo();
+ }
+ }
+
+ this.output = writer;
+
+ // Request Device Attributes
+ this.output.printf("\033[c");
+
+ // Request xterm report window/cell dimensions in pixels
+ this.output.printf("%s", xtermReportPixelDimensions());
+
+ // 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());
+
+ // Query the screen size
+ sessionInfo.queryWindowSize();
+ setDimensions(sessionInfo.getWindowWidth(),
+ sessionInfo.getWindowHeight());
+
+ // Hang onto the window size
+ windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN,
+ sessionInfo.getWindowWidth(), sessionInfo.getWindowHeight());
+
+ reloadOptions();
+
+ // Spin up the input reader
+ eventQueue = new ArrayList<TInputEvent>();
+ readerThread = new Thread(this);
+ readerThread.start();
+
+ // Clear the screen
+ this.output.write(clearAll());
+ this.output.flush();
+ }
+
+ /**
+ * Constructor sets up state for getEvent().
+ *
+ * @param listener the object this backend needs to wake up when new
+ * input comes in
+ * @param input the InputStream underlying 'reader'. Its available()
+ * method is used to determine if reader.read() will block or not.
+ * @param reader a Reader connected to the remote user.
+ * @param writer a PrintWriter connected to the remote user.
+ * @throws IllegalArgumentException if input, reader, or writer are null.
+ */
+ public ECMA48Terminal(final Object listener, final InputStream input,
+ final Reader reader, final PrintWriter writer) {
+
+ this(listener, input, reader, writer, false);
+ }
+
+ // ------------------------------------------------------------------------
+ // LogicalScreen ----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Set the window title.
+ *
+ * @param title the new title
+ */
+ @Override
+ public void setTitle(final String title) {
+ output.write(getSetTitleString(title));
+ flush();
+ }
+
+ /**
+ * Push the logical screen to the physical device.
+ */
+ @Override
+ public void flushPhysical() {
+ StringBuilder sb = new StringBuilder();
+ if ((cursorVisible)
+ && (cursorY >= 0)
+ && (cursorX >= 0)
+ && (cursorY <= height - 1)
+ && (cursorX <= width - 1)
+ ) {
+ flushString(sb);
+ sb.append(cursor(true));
+ sb.append(gotoXY(cursorX, cursorY));
+ } else {
+ sb.append(cursor(false));
+ flushString(sb);
+ }
+ output.write(sb.toString());
+ flush();
+ }
+
+ /**
+ * Resize the physical screen to match the logical screen dimensions.
+ */
+ @Override
+ public void resizeToScreen() {
+ // Send dtterm/xterm sequences, which will probably not work because
+ // allowWindowOps is defaulted to false.
+ String resizeString = String.format("\033[8;%d;%dt", getHeight(),
+ getWidth());
+ this.output.write(resizeString);
+ this.output.flush();
+ }
+
+ // ------------------------------------------------------------------------
+ // TerminalReader ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Check if there are events in the queue.
+ *
+ * @return if true, getEvents() has something to return to the backend
+ */
+ public boolean hasEvents() {
+ synchronized (eventQueue) {
+ return (eventQueue.size() > 0);
+ }
+ }
+
+ /**
+ * Return any events in the IO queue.
+ *
+ * @param queue list to append new events to
+ */
+ public void getEvents(final List<TInputEvent> queue) {
+ synchronized (eventQueue) {
+ if (eventQueue.size() > 0) {
+ synchronized (queue) {
+ queue.addAll(eventQueue);
+ }
+ eventQueue.clear();
+ }
+ }
+ }
+
+ /**
+ * Restore terminal to normal state.
+ */
+ public void closeTerminal() {
+
+ // System.err.println("=== closeTerminal() ==="); System.err.flush();
+
+ // Tell the reader thread to stop looking at input
+ stopReaderThread = true;
+ try {
+ readerThread.join();
+ } catch (InterruptedException e) {
+ if (debugToStderr) {
+ e.printStackTrace();
+ }
+ }
+
+ // Disable mouse reporting and show cursor. Defensive null check
+ // here in case closeTerminal() is called twice.
+ if (output != null) {
+ output.printf("%s%s%s%s", mouse(false), cursor(true),
+ defaultColor(), xtermResetSixelSettings());
+ output.flush();
+ }
+
+ if (setRawMode) {
+ sttyCooked();
+ setRawMode = false;
+ // We don't close System.in/out
+ } else {
+ // Shut down the streams, this should wake up the reader thread
+ // and make it exit.
+ if (input != null) {
+ try {
+ input.close();
+ } catch (IOException e) {
+ // SQUASH
+ }
+ input = null;
+ }
+ if (output != null) {
+ output.close();
+ output = null;
+ }
+ }
+ }
+
+ /**
+ * 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) {
+ this.listener = listener;
+ }
+
+ /**
+ * Reload options from System properties.
+ */
+ public void reloadOptions() {
+ // Permit RGB colors only if externally requested.
+ if (System.getProperty("jexer.ECMA48.rgbColor",
+ "false").equals("true")
+ ) {
+ doRgbColor = true;
+ } else {
+ doRgbColor = false;
+ }
+
+ // Default to using images for full-width characters.
+ if (System.getProperty("jexer.ECMA48.wideCharImages",
+ "true").equals("true")) {
+ wideCharImages = true;
+ } else {
+ wideCharImages = false;
+ }
+
+ // Pull the system properties for sixel output.
+ if (System.getProperty("jexer.ECMA48.sixel", "true").equals("true")) {
+ sixel = true;
+ } else {
+ sixel = false;
+ }
+
+ // Palette size
+ int paletteSize = 1024;
+ try {
+ paletteSize = Integer.parseInt(System.getProperty(
+ "jexer.ECMA48.sixelPaletteSize", "1024"));
+ switch (paletteSize) {
+ case 2:
+ case 256:
+ case 512:
+ case 1024:
+ case 2048:
+ sixelPaletteSize = paletteSize;
+ break;
+ default:
+ // Ignore value
+ break;
+ }
+ } catch (NumberFormatException e) {
+ // SQUASH
+ }
+
+ // Default to using images for full-width characters.
+ if (System.getProperty("jexer.ECMA48.iTerm2Images",
+ "false").equals("true")) {
+ iterm2Images = true;
+ } else {
+ iterm2Images = false;
+ }
+
+ // Set custom colors
+ setCustomSystemColors();
+ }
+
+ // ------------------------------------------------------------------------
+ // Runnable ---------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Read function runs on a separate thread.
+ */
+ public void run() {
+ boolean done = false;
+ // available() will often return > 1, so we need to read in chunks to
+ // stay caught up.
+ char [] readBuffer = new char[128];
+ List<TInputEvent> events = new ArrayList<TInputEvent>();
+
+ while (!done && !stopReaderThread) {
+ try {
+ // We assume that if inputStream has bytes available, then
+ // input won't block on read().
+ int n = inputStream.available();
+
+ /*
+ System.err.printf("inputStream.available(): %d\n", n);
+ System.err.flush();
+ */
+
+ if (n > 0) {
+ if (readBuffer.length < n) {
+ // The buffer wasn't big enough, make it huger
+ readBuffer = new char[readBuffer.length * 2];
+ }
+
+ // System.err.printf("BEFORE read()\n"); System.err.flush();
+
+ int rc = input.read(readBuffer, 0, readBuffer.length);
+
+ /*
+ System.err.printf("AFTER read() %d\n", rc);
+ System.err.flush();
+ */
+
+ if (rc == -1) {
+ // This is EOF
+ done = true;
+ } else {
+ for (int i = 0; i < rc; i++) {
+ int ch = readBuffer[i];
+ processChar(events, (char)ch);
+ }
+ getIdleEvents(events);
+ if (events.size() > 0) {
+ // Add to the queue for the backend thread to
+ // be able to obtain.
+ synchronized (eventQueue) {
+ eventQueue.addAll(events);
+ }
+ if (listener != null) {
+ synchronized (listener) {
+ listener.notifyAll();
+ }
+ }
+ events.clear();
+ }
+ }
+ } else {
+ getIdleEvents(events);
+ if (events.size() > 0) {
+ synchronized (eventQueue) {
+ eventQueue.addAll(events);
+ }
+ if (listener != null) {
+ synchronized (listener) {
+ listener.notifyAll();
+ }
+ }
+ events.clear();
+ }
+
+ if (output.checkError()) {
+ // This is EOF.
+ done = true;
+ }
+
+ // Wait 20 millis for more data
+ Thread.sleep(20);
+ }
+ // System.err.println("end while loop"); System.err.flush();
+ } catch (InterruptedException e) {
+ // SQUASH
+ } catch (IOException e) {
+ e.printStackTrace();
+ done = true;
+ }
+ } // while ((done == false) && (stopReaderThread == false))
+
+ // Pass an event up to TApplication to tell it this Backend is done.
+ synchronized (eventQueue) {
+ eventQueue.add(new TCommandEvent(cmBackendDisconnect));
+ }
+ if (listener != null) {
+ synchronized (listener) {
+ listener.notifyAll();
+ }
+ }
+
+ // System.err.println("*** run() exiting..."); System.err.flush();
+ }
+
+ // ------------------------------------------------------------------------
+ // ECMA48Terminal ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get the width of a character cell in pixels.
+ *
+ * @return the width in pixels of a character cell
+ */
+ public int getTextWidth() {
+ return (widthPixels / sessionInfo.getWindowWidth());
+ }
+
+ /**
+ * Get the height of a character cell in pixels.
+ *
+ * @return the height in pixels of a character cell
+ */
+ public int getTextHeight() {
+ return (heightPixels / sessionInfo.getWindowHeight());
+ }
+
+ /**
+ * Getter for sessionInfo.
+ *
+ * @return the SessionInfo
+ */
+ public SessionInfo getSessionInfo() {
+ return sessionInfo;
+ }
+
+ /**
+ * Get the output writer.
+ *
+ * @return the Writer
+ */
+ public PrintWriter getOutput() {
+ return output;
+ }
+
+ /**
+ * Call 'stty' to set cooked mode.
+ *
+ * <p>Actually executes '/bin/sh -c stty sane cooked < /dev/tty'
+ */
+ private void sttyCooked() {
+ doStty(false);
+ }
+
+ /**
+ * Call 'stty' to set raw mode.
+ *
+ * <p>Actually executes '/bin/sh -c stty -ignbrk -brkint -parmrk -istrip
+ * -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten
+ * -parenb cs8 min 1 < /dev/tty'
+ */
+ private void sttyRaw() {
+ doStty(true);
+ }
+
+ /**
+ * Call 'stty' to set raw or cooked mode.
+ *
+ * @param mode if true, set raw mode, otherwise set cooked mode
+ */
+ private void doStty(final boolean mode) {
+ String [] cmdRaw = {
+ "/bin/sh", "-c", "stty -ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten -parenb cs8 min 1 < /dev/tty"
+ };
+ String [] cmdCooked = {
+ "/bin/sh", "-c", "stty sane cooked < /dev/tty"
+ };
+ try {
+ Process process;
+ if (mode) {
+ process = Runtime.getRuntime().exec(cmdRaw);
+ } else {
+ process = Runtime.getRuntime().exec(cmdCooked);
+ }
+ BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8"));
+ String line = in.readLine();
+ if ((line != null) && (line.length() > 0)) {
+ System.err.println("WEIRD?! Normal output from stty: " + line);
+ }
+ while (true) {
+ BufferedReader err = new BufferedReader(new InputStreamReader(process.getErrorStream(), "UTF-8"));
+ line = err.readLine();
+ if ((line != null) && (line.length() > 0)) {
+ System.err.println("Error output from stty: " + line);
+ }
+ try {
+ process.waitFor();
+ break;
+ } catch (InterruptedException e) {
+ if (debugToStderr) {
+ e.printStackTrace();
+ }
+ }
+ }
+ int rc = process.exitValue();
+ if (rc != 0) {
+ System.err.println("stty returned error code: " + rc);
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Flush output.
+ */
+ public void flush() {
+ output.flush();
+ }
+
+ /**
+ * Perform a somewhat-optimal rendering of a line.
+ *
+ * @param y row coordinate. 0 is the top-most row.
+ * @param sb StringBuilder to write escape sequences to
+ * @param lastAttr cell attributes from the last call to flushLine
+ */
+ private void flushLine(final int y, final StringBuilder sb,
+ CellAttributes lastAttr) {
+
+ int lastX = -1;
+ int textEnd = 0;
+ for (int x = 0; x < width; x++) {
+ Cell lCell = logical[x][y];
+ if (!lCell.isBlank()) {
+ textEnd = x;
+ }
+ }
+ // Push textEnd to first column beyond the text area
+ textEnd++;
+
+ // DEBUG
+ // reallyCleared = true;
+
+ boolean hasImage = false;
+
+ for (int x = 0; x < width; x++) {
+ Cell lCell = logical[x][y];
+ Cell pCell = physical[x][y];
+
+ if (!lCell.equals(pCell) || reallyCleared) {
+
+ if (debugToStderr) {
+ System.err.printf("\n--\n");
+ System.err.printf(" Y: %d X: %d\n", y, x);
+ System.err.printf(" lCell: %s\n", lCell);
+ System.err.printf(" pCell: %s\n", pCell);
+ System.err.printf(" ==== \n");
+ }
+
+ if (lastAttr == null) {
+ lastAttr = new CellAttributes();
+ sb.append(normal());
+ }
+
+ // Place the cell
+ if ((lastX != (x - 1)) || (lastX == -1)) {
+ // Advancing at least one cell, or the first gotoXY
+ sb.append(gotoXY(x, y));
+ }
+
+ assert (lastAttr != null);
+
+ if ((x == textEnd) && (textEnd < width - 1)) {
+ assert (lCell.isBlank());
+
+ for (int i = x; i < width; i++) {
+ assert (logical[i][y].isBlank());
+ // Physical is always updated
+ physical[i][y].reset();
+ }
+
+ // Clear remaining line
+ sb.append(clearRemainingLine());
+ lastAttr.reset();
+ return;
+ }
+
+ // Image cell: bypass the rest of the loop, it is not
+ // rendered here.
+ if ((wideCharImages && lCell.isImage())
+ || (!wideCharImages
+ && lCell.isImage()
+ && (lCell.getWidth() == Cell.Width.SINGLE))
+ ) {
+ hasImage = true;
+
+ // Save the last rendered cell
+ lastX = x;
+
+ // Physical is always updated
+ physical[x][y].setTo(lCell);
+ continue;
+ }
+
+ assert ((wideCharImages && !lCell.isImage())
+ || (!wideCharImages
+ && (!lCell.isImage()
+ || (lCell.isImage()
+ && (lCell.getWidth() != Cell.Width.SINGLE)))));
+
+ if (!wideCharImages && (lCell.getWidth() == Cell.Width.RIGHT)) {
+ continue;
+ }
+
+ if (hasImage) {
+ hasImage = false;
+ sb.append(gotoXY(x, y));
+ }
+
+ // Now emit only the modified attributes
+ if ((lCell.getForeColor() != lastAttr.getForeColor())
+ && (lCell.getBackColor() != lastAttr.getBackColor())
+ && (!lCell.isRGB())
+ && (lCell.isBold() == lastAttr.isBold())
+ && (lCell.isReverse() == lastAttr.isReverse())
+ && (lCell.isUnderline() == lastAttr.isUnderline())
+ && (lCell.isBlink() == lastAttr.isBlink())
+ ) {
+ // Both colors changed, attributes the same
+ sb.append(color(lCell.isBold(),
+ lCell.getForeColor(), lCell.getBackColor()));
+
+ if (debugToStderr) {
+ System.err.printf("1 Change only fore/back colors\n");
+ }
+
+ } else if (lCell.isRGB()
+ && (lCell.getForeColorRGB() != lastAttr.getForeColorRGB())
+ && (lCell.getBackColorRGB() != lastAttr.getBackColorRGB())
+ && (lCell.isBold() == lastAttr.isBold())
+ && (lCell.isReverse() == lastAttr.isReverse())
+ && (lCell.isUnderline() == lastAttr.isUnderline())
+ && (lCell.isBlink() == lastAttr.isBlink())
+ ) {
+ // Both colors changed, attributes the same
+ sb.append(colorRGB(lCell.getForeColorRGB(),
+ lCell.getBackColorRGB()));
+
+ if (debugToStderr) {
+ System.err.printf("1 Change only fore/back colors (RGB)\n");
+ }
+ } else if ((lCell.getForeColor() != lastAttr.getForeColor())
+ && (lCell.getBackColor() != lastAttr.getBackColor())
+ && (!lCell.isRGB())
+ && (lCell.isBold() != lastAttr.isBold())
+ && (lCell.isReverse() != lastAttr.isReverse())
+ && (lCell.isUnderline() != lastAttr.isUnderline())
+ && (lCell.isBlink() != lastAttr.isBlink())
+ ) {
+ // Everything is different
+ sb.append(color(lCell.getForeColor(),
+ lCell.getBackColor(),
+ lCell.isBold(), lCell.isReverse(),
+ lCell.isBlink(),
+ lCell.isUnderline()));
+
+ if (debugToStderr) {
+ System.err.printf("2 Set all attributes\n");
+ }
+ } else if ((lCell.getForeColor() != lastAttr.getForeColor())
+ && (lCell.getBackColor() == lastAttr.getBackColor())
+ && (!lCell.isRGB())
+ && (lCell.isBold() == lastAttr.isBold())
+ && (lCell.isReverse() == lastAttr.isReverse())
+ && (lCell.isUnderline() == lastAttr.isUnderline())
+ && (lCell.isBlink() == lastAttr.isBlink())
+ ) {
+
+ // Attributes same, foreColor different
+ sb.append(color(lCell.isBold(),
+ lCell.getForeColor(), true));
+
+ if (debugToStderr) {
+ System.err.printf("3 Change foreColor\n");
+ }
+ } else if (lCell.isRGB()
+ && (lCell.getForeColorRGB() != lastAttr.getForeColorRGB())
+ && (lCell.getBackColorRGB() == lastAttr.getBackColorRGB())
+ && (lCell.getForeColorRGB() >= 0)
+ && (lCell.getBackColorRGB() >= 0)
+ && (lCell.isBold() == lastAttr.isBold())
+ && (lCell.isReverse() == lastAttr.isReverse())
+ && (lCell.isUnderline() == lastAttr.isUnderline())
+ && (lCell.isBlink() == lastAttr.isBlink())
+ ) {
+ // Attributes same, foreColor different
+ sb.append(colorRGB(lCell.getForeColorRGB(), true));
+
+ if (debugToStderr) {
+ System.err.printf("3 Change foreColor (RGB)\n");
+ }
+ } else if ((lCell.getForeColor() == lastAttr.getForeColor())
+ && (lCell.getBackColor() != lastAttr.getBackColor())
+ && (!lCell.isRGB())
+ && (lCell.isBold() == lastAttr.isBold())
+ && (lCell.isReverse() == lastAttr.isReverse())
+ && (lCell.isUnderline() == lastAttr.isUnderline())
+ && (lCell.isBlink() == lastAttr.isBlink())
+ ) {
+ // Attributes same, backColor different
+ sb.append(color(lCell.isBold(),
+ lCell.getBackColor(), false));
+
+ if (debugToStderr) {
+ System.err.printf("4 Change backColor\n");
+ }
+ } else if (lCell.isRGB()
+ && (lCell.getForeColorRGB() == lastAttr.getForeColorRGB())
+ && (lCell.getBackColorRGB() != lastAttr.getBackColorRGB())
+ && (lCell.isBold() == lastAttr.isBold())
+ && (lCell.isReverse() == lastAttr.isReverse())
+ && (lCell.isUnderline() == lastAttr.isUnderline())
+ && (lCell.isBlink() == lastAttr.isBlink())
+ ) {
+ // Attributes same, foreColor different
+ sb.append(colorRGB(lCell.getBackColorRGB(), false));
+
+ if (debugToStderr) {
+ System.err.printf("4 Change backColor (RGB)\n");
+ }
+ } else if ((lCell.getForeColor() == lastAttr.getForeColor())
+ && (lCell.getBackColor() == lastAttr.getBackColor())
+ && (lCell.getForeColorRGB() == lastAttr.getForeColorRGB())
+ && (lCell.getBackColorRGB() == lastAttr.getBackColorRGB())
+ && (lCell.isBold() == lastAttr.isBold())
+ && (lCell.isReverse() == lastAttr.isReverse())
+ && (lCell.isUnderline() == lastAttr.isUnderline())
+ && (lCell.isBlink() == lastAttr.isBlink())
+ ) {
+
+ // All attributes the same, just print the char
+ // NOP
+
+ if (debugToStderr) {
+ System.err.printf("5 Only emit character\n");
+ }
+ } else {
+ // Just reset everything again
+ if (!lCell.isRGB()) {
+ sb.append(color(lCell.getForeColor(),
+ lCell.getBackColor(),
+ lCell.isBold(),
+ lCell.isReverse(),
+ lCell.isBlink(),
+ lCell.isUnderline()));
+
+ if (debugToStderr) {
+ System.err.printf("6 Change all attributes\n");
+ }
+ } else {
+ sb.append(colorRGB(lCell.getForeColorRGB(),
+ lCell.getBackColorRGB(),
+ lCell.isBold(),
+ lCell.isReverse(),
+ lCell.isBlink(),
+ lCell.isUnderline()));
+ if (debugToStderr) {
+ System.err.printf("6 Change all attributes (RGB)\n");
+ }
+ }
+
+ }
+ // Emit the character
+ if (wideCharImages
+ // Don't emit the right-half of full-width chars.
+ || (!wideCharImages
+ && (lCell.getWidth() != Cell.Width.RIGHT))
+ ) {
+ sb.append(Character.toChars(lCell.getChar()));
+ }
+
+ // Save the last rendered cell
+ lastX = x;
+ lastAttr.setTo(lCell);
+
+ // Physical is always updated
+ physical[x][y].setTo(lCell);
+
+ } // if (!lCell.equals(pCell) || (reallyCleared == true))
+
+ } // for (int x = 0; x < width; x++)
+ }
+
+ /**
+ * Render the screen to a string that can be emitted to something that
+ * knows how to process ECMA-48/ANSI X3.64 escape sequences.
+ *
+ * @param sb StringBuilder to write escape sequences to
+ * @return escape sequences string that provides the updates to the
+ * physical screen
+ */
+ private String flushString(final StringBuilder sb) {
+ CellAttributes attr = null;
+
+ if (reallyCleared) {
+ attr = new CellAttributes();
+ sb.append(clearAll());
+ }
+
+ /*
+ * For images support, draw all of the image output first, and then
+ * draw everything else afterwards. This works OK, but performance
+ * is still a drag on larger pictures.
+ */
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ // If physical had non-image data that is now image data, the
+ // entire row must be redrawn.
+ Cell lCell = logical[x][y];
+ Cell pCell = physical[x][y];
+ if (lCell.isImage() && !pCell.isImage()) {
+ unsetImageRow(y);
+ break;
+ }
+ }
+ }
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ Cell lCell = logical[x][y];
+ Cell pCell = physical[x][y];
+
+ if (!lCell.isImage()
+ || (!wideCharImages
+ && (lCell.getWidth() != Cell.Width.SINGLE))
+ ) {
+ continue;
+ }
+
+ int left = x;
+ int right = x;
+ while ((right < width)
+ && (logical[right][y].isImage())
+ && (!logical[right][y].equals(physical[right][y])
+ || reallyCleared)
+ ) {
+ right++;
+ }
+ ArrayList<Cell> cellsToDraw = new ArrayList<Cell>();
+ for (int i = 0; i < (right - x); i++) {
+ assert (logical[x + i][y].isImage());
+ cellsToDraw.add(logical[x + i][y]);
+
+ // Physical is always updated.
+ physical[x + i][y].setTo(lCell);
+ }
+ if (cellsToDraw.size() > 0) {
+ if (iterm2Images) {
+ sb.append(toIterm2Image(x, y, cellsToDraw));
+ } else if (jexerImages) {
+ sb.append(toJexerImage(x, y, cellsToDraw));
+ } else {
+ sb.append(toSixel(x, y, cellsToDraw));
+ }
+ }
+
+ x = right;
+ }
+ }
+
+ // Draw the text part now.
+ for (int y = 0; y < height; y++) {
+ flushLine(y, sb, attr);
+ }
+
+ reallyCleared = false;
+
+ String result = sb.toString();
+ if (debugToStderr) {
+ System.err.printf("flushString(): %s\n", result);
+ }
+ return result;
+ }
+
+ /**
+ * Reset keyboard/mouse input parser.
+ */
+ private void resetParser() {
+ state = ParseState.GROUND;
+ params = new ArrayList<String>();
+ params.clear();
+ params.add("");
+ decPrivateModeFlag = false;
+ }
+
+ /**
+ * Produce a control character or one of the special ones (ENTER, TAB,
+ * etc.).
+ *
+ * @param ch Unicode code point
+ * @param alt if true, set alt on the TKeypress
+ * @return one TKeypress event, either a control character (e.g. isKey ==
+ * false, ch == 'A', ctrl == true), or a special key (e.g. isKey == true,
+ * fnKey == ESC)
+ */
+ private TKeypressEvent controlChar(final char ch, final boolean alt) {
+ // System.err.printf("controlChar: %02x\n", ch);
+
+ switch (ch) {
+ case 0x0D:
+ // Carriage return --> ENTER
+ return new TKeypressEvent(kbEnter, alt, false, false);
+ case 0x0A:
+ // Linefeed --> ENTER
+ return new TKeypressEvent(kbEnter, alt, false, false);
+ case 0x1B:
+ // ESC
+ return new TKeypressEvent(kbEsc, alt, false, false);
+ case '\t':
+ // TAB
+ return new TKeypressEvent(kbTab, alt, false, false);
+ default:
+ // Make all other control characters come back as the alphabetic
+ // character with the ctrl field set. So SOH would be 'A' +
+ // ctrl.
+ return new TKeypressEvent(false, 0, (char)(ch + 0x40),
+ alt, true, false);
+ }
+ }
+
+ /**
+ * Produce special key from CSI Pn ; Pm ; ... ~
+ *
+ * @return one KEYPRESS event representing a special key
+ */
+ private TInputEvent csiFnKey() {
+ int key = 0;
+ if (params.size() > 0) {
+ key = Integer.parseInt(params.get(0));
+ }
+ boolean alt = false;
+ boolean ctrl = false;
+ boolean shift = false;
+ if (params.size() > 1) {
+ shift = csiIsShift(params.get(1));
+ alt = csiIsAlt(params.get(1));
+ ctrl = csiIsCtrl(params.get(1));
+ }
+
+ switch (key) {
+ case 1:
+ return new TKeypressEvent(kbHome, alt, ctrl, shift);
+ case 2:
+ return new TKeypressEvent(kbIns, alt, ctrl, shift);
+ case 3:
+ return new TKeypressEvent(kbDel, alt, ctrl, shift);
+ case 4:
+ return new TKeypressEvent(kbEnd, alt, ctrl, shift);
+ case 5:
+ return new TKeypressEvent(kbPgUp, alt, ctrl, shift);
+ case 6:
+ return new TKeypressEvent(kbPgDn, alt, ctrl, shift);
+ case 15:
+ return new TKeypressEvent(kbF5, alt, ctrl, shift);
+ case 17:
+ return new TKeypressEvent(kbF6, alt, ctrl, shift);
+ case 18:
+ return new TKeypressEvent(kbF7, alt, ctrl, shift);
+ case 19:
+ return new TKeypressEvent(kbF8, alt, ctrl, shift);
+ case 20:
+ return new TKeypressEvent(kbF9, alt, ctrl, shift);
+ case 21:
+ return new TKeypressEvent(kbF10, alt, ctrl, shift);
+ case 23:
+ return new TKeypressEvent(kbF11, alt, ctrl, shift);
+ case 24:
+ return new TKeypressEvent(kbF12, alt, ctrl, shift);
+ default:
+ // Unknown
+ return null;
+ }
+ }
+
+ /**
+ * Produce mouse events based on "Any event tracking" and UTF-8
+ * coordinates. See
+ * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
+ *
+ * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
+ */
+ private TInputEvent parseMouse() {
+ int buttons = params.get(0).charAt(0) - 32;
+ int x = params.get(0).charAt(1) - 32 - 1;
+ int y = params.get(0).charAt(2) - 32 - 1;
+
+ // Clamp X and Y to the physical screen coordinates.
+ if (x >= windowResize.getWidth()) {
+ x = windowResize.getWidth() - 1;
+ }
+ if (y >= windowResize.getHeight()) {
+ y = windowResize.getHeight() - 1;
+ }
+
+ TMouseEvent.Type eventType = TMouseEvent.Type.MOUSE_DOWN;
+ boolean eventMouse1 = false;
+ boolean eventMouse2 = false;
+ boolean eventMouse3 = false;
+ boolean eventMouseWheelUp = false;
+ boolean eventMouseWheelDown = false;
+
+ // System.err.printf("buttons: %04x\r\n", buttons);
+
+ switch (buttons) {
+ case 0:
+ eventMouse1 = true;
+ mouse1 = true;
+ break;
+ case 1:
+ eventMouse2 = true;
+ mouse2 = true;
+ break;
+ case 2:
+ eventMouse3 = true;
+ mouse3 = true;
+ break;
+ case 3:
+ // Release or Move
+ if (!mouse1 && !mouse2 && !mouse3) {
+ eventType = TMouseEvent.Type.MOUSE_MOTION;
+ } else {
+ eventType = TMouseEvent.Type.MOUSE_UP;
+ }
+ if (mouse1) {
+ mouse1 = false;
+ eventMouse1 = true;
+ }
+ if (mouse2) {
+ mouse2 = false;
+ eventMouse2 = true;
+ }
+ if (mouse3) {
+ mouse3 = false;
+ eventMouse3 = true;
+ }
+ break;
+
+ case 32:
+ // Dragging with mouse1 down
+ eventMouse1 = true;
+ mouse1 = true;
+ eventType = TMouseEvent.Type.MOUSE_MOTION;
+ break;
+
+ case 33:
+ // Dragging with mouse2 down
+ eventMouse2 = true;
+ mouse2 = true;
+ eventType = TMouseEvent.Type.MOUSE_MOTION;
+ break;
+
+ case 34:
+ // Dragging with mouse3 down
+ eventMouse3 = true;
+ mouse3 = true;
+ eventType = TMouseEvent.Type.MOUSE_MOTION;
+ break;
+
+ case 96:
+ // Dragging with mouse2 down after wheelUp
+ eventMouse2 = true;
+ mouse2 = true;
+ eventType = TMouseEvent.Type.MOUSE_MOTION;
+ break;
+
+ case 97:
+ // Dragging with mouse2 down after wheelDown
+ eventMouse2 = true;
+ mouse2 = true;
+ eventType = TMouseEvent.Type.MOUSE_MOTION;
+ break;
+
+ case 64:
+ eventMouseWheelUp = true;
+ break;
+
+ case 65:
+ eventMouseWheelDown = true;
+ break;
+
+ default:
+ // Unknown, just make it motion
+ eventType = TMouseEvent.Type.MOUSE_MOTION;
+ break;
+ }
+ return new TMouseEvent(eventType, x, y, x, y,
+ eventMouse1, eventMouse2, eventMouse3,
+ eventMouseWheelUp, eventMouseWheelDown);
+ }
+
+ /**
+ * Produce mouse events based on "Any event tracking" and SGR
+ * coordinates. See
+ * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
+ *
+ * @param release if true, this was a release ('m')
+ * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
+ */
+ private TInputEvent parseMouseSGR(final boolean release) {
+ // SGR extended coordinates - mode 1006
+ if (params.size() < 3) {
+ // Invalid position, bail out.
+ return null;
+ }
+ int buttons = Integer.parseInt(params.get(0));
+ int x = Integer.parseInt(params.get(1)) - 1;
+ int y = Integer.parseInt(params.get(2)) - 1;
+
+ // Clamp X and Y to the physical screen coordinates.
+ if (x >= windowResize.getWidth()) {
+ x = windowResize.getWidth() - 1;
+ }
+ if (y >= windowResize.getHeight()) {
+ y = windowResize.getHeight() - 1;
+ }
+
+ TMouseEvent.Type eventType = TMouseEvent.Type.MOUSE_DOWN;
+ boolean eventMouse1 = false;
+ boolean eventMouse2 = false;
+ boolean eventMouse3 = false;
+ boolean eventMouseWheelUp = false;
+ boolean eventMouseWheelDown = false;
+
+ if (release) {
+ eventType = TMouseEvent.Type.MOUSE_UP;
+ }
+
+ switch (buttons) {
+ case 0:
+ eventMouse1 = true;
+ break;
+ case 1:
+ eventMouse2 = true;
+ break;
+ case 2:
+ eventMouse3 = true;
+ break;
+ case 35:
+ // Motion only, no buttons down
+ eventType = TMouseEvent.Type.MOUSE_MOTION;
+ break;
+
+ case 32:
+ // Dragging with mouse1 down
+ eventMouse1 = true;
+ eventType = TMouseEvent.Type.MOUSE_MOTION;
+ break;
+
+ case 33:
+ // Dragging with mouse2 down
+ eventMouse2 = true;
+ eventType = TMouseEvent.Type.MOUSE_MOTION;
+ break;
+
+ case 34:
+ // Dragging with mouse3 down
+ eventMouse3 = true;
+ eventType = TMouseEvent.Type.MOUSE_MOTION;
+ break;
+
+ case 96:
+ // Dragging with mouse2 down after wheelUp
+ eventMouse2 = true;
+ eventType = TMouseEvent.Type.MOUSE_MOTION;
+ break;
+
+ case 97:
+ // Dragging with mouse2 down after wheelDown
+ eventMouse2 = true;
+ eventType = TMouseEvent.Type.MOUSE_MOTION;
+ break;
+
+ case 64:
+ eventMouseWheelUp = true;
+ break;
+
+ case 65:
+ eventMouseWheelDown = true;
+ break;
+
+ default:
+ // Unknown, bail out
+ return null;
+ }
+ return new TMouseEvent(eventType, x, y, x, y,
+ eventMouse1, eventMouse2, eventMouse3,
+ eventMouseWheelUp, eventMouseWheelDown);
+ }
+
+ /**
+ * Return any events in the IO queue due to timeout.
+ *
+ * @param queue list to append new events to
+ */
+ private void getIdleEvents(final List<TInputEvent> queue) {
+ long nowTime = System.currentTimeMillis();
+
+ // Check for new window size
+ long windowSizeDelay = nowTime - windowSizeTime;
+ if (windowSizeDelay > 1000) {
+ int oldTextWidth = getTextWidth();
+ int oldTextHeight = getTextHeight();
+
+ sessionInfo.queryWindowSize();
+ int newWidth = sessionInfo.getWindowWidth();
+ int newHeight = sessionInfo.getWindowHeight();
+
+ if ((newWidth != windowResize.getWidth())
+ || (newHeight != windowResize.getHeight())
+ ) {
+
+ // Request xterm report window dimensions in pixels again.
+ // Between now and then, ensure that the reported text cell
+ // size is the same by setting widthPixels and heightPixels
+ // to match the new dimensions.
+ widthPixels = oldTextWidth * newWidth;
+ heightPixels = oldTextHeight * newHeight;
+
+ if (debugToStderr) {
+ System.err.println("Screen size changed, old size " +
+ windowResize);
+ System.err.println(" new size " +
+ newWidth + " x " + newHeight);
+ System.err.println(" old pixels " +
+ oldTextWidth + " x " + oldTextHeight);
+ System.err.println(" new pixels " +
+ getTextWidth() + " x " + getTextHeight());
+ }
+
+ this.output.printf("%s", xtermReportPixelDimensions());
+ this.output.flush();
+
+ TResizeEvent event = new TResizeEvent(TResizeEvent.Type.SCREEN,
+ newWidth, newHeight);
+ windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN,
+ newWidth, newHeight);
+ queue.add(event);
+ }
+ windowSizeTime = nowTime;
+ }
+
+ // ESCDELAY type timeout
+ if (state == ParseState.ESCAPE) {
+ long escDelay = nowTime - escapeTime;
+ if (escDelay > 100) {
+ // After 0.1 seconds, assume a true escape character
+ queue.add(controlChar((char)0x1B, false));
+ resetParser();
+ }
+ }
+ }
+
+ /**
+ * Returns true if the CSI parameter for a keyboard command means that
+ * shift was down.
+ */
+ private boolean csiIsShift(final String x) {
+ if ((x.equals("2"))
+ || (x.equals("4"))
+ || (x.equals("6"))
+ || (x.equals("8"))
+ ) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the CSI parameter for a keyboard command means that
+ * alt was down.
+ */
+ private boolean csiIsAlt(final String x) {
+ if ((x.equals("3"))
+ || (x.equals("4"))
+ || (x.equals("7"))
+ || (x.equals("8"))
+ ) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the CSI parameter for a keyboard command means that
+ * ctrl was down.
+ */
+ private boolean csiIsCtrl(final String x) {
+ if ((x.equals("5"))
+ || (x.equals("6"))
+ || (x.equals("7"))
+ || (x.equals("8"))
+ ) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Parses the next character of input to see if an InputEvent is
+ * fully here.
+ *
+ * @param events list to append new events to
+ * @param ch Unicode code point
+ */
+ private void processChar(final List<TInputEvent> events, final char ch) {
+
+ // ESCDELAY type timeout
+ long nowTime = System.currentTimeMillis();
+ if (state == ParseState.ESCAPE) {
+ long escDelay = nowTime - escapeTime;
+ if (escDelay > 250) {
+ // After 0.25 seconds, assume a true escape character
+ events.add(controlChar((char)0x1B, false));
+ resetParser();
+ }
+ }
+
+ // TKeypress fields
+ boolean ctrl = false;
+ boolean alt = false;
+ boolean shift = false;
+
+ // System.err.printf("state: %s ch %c\r\n", state, ch);
+
+ switch (state) {
+ case GROUND:
+
+ if (ch == 0x1B) {
+ state = ParseState.ESCAPE;
+ escapeTime = nowTime;
+ return;
+ }
+
+ if (ch <= 0x1F) {
+ // Control character
+ events.add(controlChar(ch, false));
+ resetParser();
+ return;
+ }
+
+ if (ch >= 0x20) {
+ // Normal character
+ events.add(new TKeypressEvent(false, 0, ch,
+ false, false, false));
+ resetParser();
+ return;
+ }
+
+ break;
+
+ case ESCAPE:
+ if (ch <= 0x1F) {
+ // ALT-Control character
+ events.add(controlChar(ch, true));
+ resetParser();
+ return;
+ }
+
+ if (ch == 'O') {
+ // This will be one of the function keys
+ state = ParseState.ESCAPE_INTERMEDIATE;
+ return;
+ }
+
+ // '[' goes to CSI_ENTRY
+ if (ch == '[') {
+ state = ParseState.CSI_ENTRY;
+ return;
+ }
+
+ // Everything else is assumed to be Alt-keystroke
+ if ((ch >= 'A') && (ch <= 'Z')) {
+ shift = true;
+ }
+ alt = true;
+ events.add(new TKeypressEvent(false, 0, ch, alt, ctrl, shift));
+ resetParser();
+ return;
+
+ case ESCAPE_INTERMEDIATE:
+ if ((ch >= 'P') && (ch <= 'S')) {
+ // Function key
+ switch (ch) {
+ case 'P':
+ events.add(new TKeypressEvent(kbF1));
+ break;
+ case 'Q':
+ events.add(new TKeypressEvent(kbF2));
+ break;
+ case 'R':
+ events.add(new TKeypressEvent(kbF3));
+ break;
+ case 'S':
+ events.add(new TKeypressEvent(kbF4));
+ break;
+ default:
+ break;
+ }
+ resetParser();
+ return;
+ }
+
+ // Unknown keystroke, ignore
+ resetParser();
+ return;
+
+ case CSI_ENTRY:
+ // Numbers - parameter values
+ if ((ch >= '0') && (ch <= '9')) {
+ params.set(params.size() - 1,
+ params.get(params.size() - 1) + ch);
+ state = ParseState.CSI_PARAM;
+ return;
+ }
+ // Parameter separator
+ if (ch == ';') {
+ params.add("");
+ return;
+ }
+
+ if ((ch >= 0x30) && (ch <= 0x7E)) {
+ switch (ch) {
+ case 'A':
+ // Up
+ events.add(new TKeypressEvent(kbUp, alt, ctrl, shift));
+ resetParser();
+ return;
+ case 'B':
+ // Down
+ events.add(new TKeypressEvent(kbDown, alt, ctrl, shift));
+ resetParser();
+ return;
+ case 'C':
+ // Right
+ events.add(new TKeypressEvent(kbRight, alt, ctrl, shift));
+ resetParser();
+ return;
+ case 'D':
+ // Left
+ events.add(new TKeypressEvent(kbLeft, alt, ctrl, shift));
+ resetParser();
+ return;
+ case 'H':
+ // Home
+ events.add(new TKeypressEvent(kbHome));
+ resetParser();
+ return;
+ case 'F':
+ // End
+ events.add(new TKeypressEvent(kbEnd));
+ resetParser();
+ return;
+ case 'Z':
+ // CBT - Cursor backward X tab stops (default 1)
+ events.add(new TKeypressEvent(kbBackTab));
+ resetParser();
+ return;
+ case 'M':
+ // Mouse position
+ state = ParseState.MOUSE;
+ return;
+ case '<':
+ // Mouse position, SGR (1006) coordinates
+ state = ParseState.MOUSE_SGR;
+ return;
+ case '?':
+ // DEC private mode flag
+ decPrivateModeFlag = true;
+ return;
+ default:
+ break;
+ }
+ }
+
+ // Unknown keystroke, ignore
+ resetParser();
+ return;
+
+ case MOUSE_SGR:
+ // Numbers - parameter values
+ if ((ch >= '0') && (ch <= '9')) {
+ params.set(params.size() - 1,
+ params.get(params.size() - 1) + ch);
+ return;
+ }
+ // Parameter separator
+ if (ch == ';') {
+ params.add("");
+ return;
+ }
+
+ switch (ch) {
+ case 'M':
+ // Generate a mouse press event
+ TInputEvent event = parseMouseSGR(false);
+ if (event != null) {
+ events.add(event);
+ }
+ resetParser();
+ return;
+ case 'm':
+ // Generate a mouse release event
+ event = parseMouseSGR(true);
+ if (event != null) {
+ events.add(event);
+ }
+ resetParser();
+ return;
+ default:
+ break;
+ }
+
+ // Unknown keystroke, ignore
+ resetParser();
+ return;
+
+ case CSI_PARAM:
+ // Numbers - parameter values
+ if ((ch >= '0') && (ch <= '9')) {
+ params.set(params.size() - 1,
+ params.get(params.size() - 1) + ch);
+ state = ParseState.CSI_PARAM;
+ return;
+ }
+ // Parameter separator
+ if (ch == ';') {
+ params.add("");
+ return;
+ }
+
+ if (ch == '~') {
+ events.add(csiFnKey());
+ resetParser();
+ return;
+ }
+
+ if ((ch >= 0x30) && (ch <= 0x7E)) {
+ switch (ch) {
+ case 'A':
+ // Up
+ if (params.size() > 1) {
+ shift = csiIsShift(params.get(1));
+ alt = csiIsAlt(params.get(1));
+ ctrl = csiIsCtrl(params.get(1));
+ }
+ events.add(new TKeypressEvent(kbUp, alt, ctrl, shift));
+ resetParser();
+ return;
+ case 'B':
+ // Down
+ if (params.size() > 1) {
+ shift = csiIsShift(params.get(1));
+ alt = csiIsAlt(params.get(1));
+ ctrl = csiIsCtrl(params.get(1));
+ }
+ events.add(new TKeypressEvent(kbDown, alt, ctrl, shift));
+ resetParser();
+ return;
+ case 'C':
+ // Right
+ if (params.size() > 1) {
+ shift = csiIsShift(params.get(1));
+ alt = csiIsAlt(params.get(1));
+ ctrl = csiIsCtrl(params.get(1));
+ }
+ events.add(new TKeypressEvent(kbRight, alt, ctrl, shift));
+ resetParser();
+ return;
+ case 'D':
+ // Left
+ if (params.size() > 1) {
+ shift = csiIsShift(params.get(1));
+ alt = csiIsAlt(params.get(1));
+ ctrl = csiIsCtrl(params.get(1));
+ }
+ events.add(new TKeypressEvent(kbLeft, alt, ctrl, shift));
+ resetParser();
+ return;
+ case 'H':
+ // Home
+ if (params.size() > 1) {
+ shift = csiIsShift(params.get(1));
+ alt = csiIsAlt(params.get(1));
+ ctrl = csiIsCtrl(params.get(1));
+ }
+ events.add(new TKeypressEvent(kbHome, alt, ctrl, shift));
+ resetParser();
+ return;
+ case 'F':
+ // End
+ if (params.size() > 1) {
+ shift = csiIsShift(params.get(1));
+ alt = csiIsAlt(params.get(1));
+ ctrl = csiIsCtrl(params.get(1));
+ }
+ events.add(new TKeypressEvent(kbEnd, alt, ctrl, shift));
+ resetParser();
+ return;
+ case 'c':
+ // Device Attributes
+ if (decPrivateModeFlag == false) {
+ break;
+ }
+ for (String x: params) {
+ if (x.equals("4")) {
+ // Terminal reports sixel support
+ if (debugToStderr) {
+ System.err.println("Device Attributes: sixel");
+ }
+ }
+ if (x.equals("444")) {
+ // Terminal reports Jexer images support
+ if (debugToStderr) {
+ System.err.println("Device Attributes: Jexer images");
+ }
+ jexerImages = true;
+ }
+ }
+ return;
+ case 't':
+ // windowOps
+ if ((params.size() > 2) && (params.get(0).equals("4"))) {
+ if (debugToStderr) {
+ System.err.printf("windowOp pixels: " +
+ "height %s width %s\n",
+ params.get(1), params.get(2));
+ }
+ try {
+ widthPixels = Integer.parseInt(params.get(2));
+ heightPixels = Integer.parseInt(params.get(1));
+ } catch (NumberFormatException e) {
+ if (debugToStderr) {
+ e.printStackTrace();
+ }
+ }
+ if (widthPixels <= 0) {
+ widthPixels = 640;
+ }
+ if (heightPixels <= 0) {
+ heightPixels = 400;
+ }
+ }
+ if ((params.size() > 2) && (params.get(0).equals("6"))) {
+ if (debugToStderr) {
+ System.err.printf("windowOp text cell pixels: " +
+ "height %s width %s\n",
+ params.get(1), params.get(2));
+ }
+ try {
+ widthPixels = width * Integer.parseInt(params.get(2));
+ heightPixels = height * Integer.parseInt(params.get(1));
+ } catch (NumberFormatException e) {
+ if (debugToStderr) {
+ e.printStackTrace();
+ }
+ }
+ if (widthPixels <= 0) {
+ widthPixels = 640;
+ }
+ if (heightPixels <= 0) {
+ heightPixels = 400;
+ }
+ }
+ resetParser();
+ return;
+ default:
+ break;
+ }
+ }
+
+ // Unknown keystroke, ignore
+ resetParser();
+ return;
+
+ case MOUSE:
+ params.set(0, params.get(params.size() - 1) + ch);
+ if (params.get(0).length() == 3) {
+ // We have enough to generate a mouse event
+ events.add(parseMouse());
+ resetParser();
+ }
+ return;
+
+ default:
+ break;
+ }
+
+ // This "should" be impossible to reach
+ return;
+ }
+
+ /**
+ * Request (u)xterm to use the sixel settings we need:
+ *
+ * - enable sixel scrolling
+ *
+ * - disable private color registers (so that we can use one common
+ * palette)
+ *
+ * @return the string to emit to xterm
+ */
+ private String xtermSetSixelSettings() {
+ return "\033[?80h\033[?1070l";
+ }
+
+ /**
+ * Restore (u)xterm its default sixel settings:
+ *
+ * - enable sixel scrolling
+ *
+ * - enable private color registers
+ *
+ * @return the string to emit to xterm
+ */
+ private String xtermResetSixelSettings() {
+ return "\033[?80h\033[?1070h";
+ }
+
+ /**
+ * Request (u)xterm to report the current window and cell size dimensions
+ * in pixels.
+ *
+ * @return the string to emit to xterm
+ */
+ private String xtermReportPixelDimensions() {
+ // We will ask for both window and text cell dimensions, and
+ // hopefully one of them will work.
+ return "\033[14t\033[16t";
+ }
+
+ /**
+ * Tell (u)xterm that we want alt- keystrokes to send escape + character
+ * rather than set the 8th bit. Anyone who wants UTF8 should want this
+ * enabled.
+ *
+ * @param on if true, enable metaSendsEscape
+ * @return the string to emit to xterm
+ */
+ private String xtermMetaSendsEscape(final boolean on) {
+ if (on) {
+ return "\033[?1036h\033[?1034l";
+ }
+ return "\033[?1036l";
+ }
+
+ /**
+ * Create an xterm OSC sequence to change the window title.
+ *
+ * @param title the new title
+ * @return the string to emit to xterm
+ */
+ private String getSetTitleString(final String title) {
+ return "\033]2;" + title + "\007";
+ }
+
+ // ------------------------------------------------------------------------
+ // Sixel output support ---------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get the number of colors in the sixel palette.
+ *
+ * @return the palette size
+ */
+ public int getSixelPaletteSize() {
+ return sixelPaletteSize;
+ }
+
+ /**
+ * Set the number of colors in the sixel palette.
+ *
+ * @param paletteSize the new palette size
+ */
+ public void setSixelPaletteSize(final int paletteSize) {
+ if (paletteSize == sixelPaletteSize) {
+ return;
+ }
+
+ switch (paletteSize) {
+ case 2:
+ case 256:
+ case 512:
+ case 1024:
+ case 2048:
+ break;
+ default:
+ throw new IllegalArgumentException("Unsupported sixel palette " +
+ " size: " + paletteSize);
+ }
+
+ // Don't step on the screen refresh thread.
+ synchronized (this) {
+ sixelPaletteSize = paletteSize;
+ palette = null;
+ sixelCache = null;
+ clearPhysical();
+ }
+ }
+
+ /**
+ * Start a sixel string for display one row's worth of bitmap data.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @return the string to emit to an ANSI / ECMA-style terminal
+ */
+ private String startSixel(final int x, final int y) {
+ StringBuilder sb = new StringBuilder();
+
+ assert (sixel == true);
+
+ // Place the cursor
+ sb.append(gotoXY(x, y));
+
+ // DCS
+ sb.append("\033Pq");
+
+ if (palette == null) {
+ palette = new SixelPalette();
+ // TODO: make this an option (shared palette or not)
+ palette.emitPalette(sb, null);
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * End a sixel string for display one row's worth of bitmap data.
+ *
+ * @return the string to emit to an ANSI / ECMA-style terminal
+ */
+ private String endSixel() {
+ assert (sixel == true);
+
+ // ST
+ return ("\033\\");
+ }
+
+ /**
+ * Create a sixel 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 toSixel(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 (sixel == false) {
+ sb.append(normal());
+ sb.append(gotoXY(x, y));
+ for (int i = 0; i < cells.size(); i++) {
+ sb.append(' ');
+ }
+ 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.
+ sb.append(normal());
+ sb.append(gotoXY(x, y));
+ for (int j = 0; j < cells.size(); j++) {
+ sb.append(' ');
+ }
+ return sb.toString();
+ }
+
+ if (sixelCache == null) {
+ sixelCache = 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 = sixelCache.get(cells);
+ if (cachedResult != null) {
+ // System.err.println("CACHE HIT");
+ sb.append(startSixel(x, y));
+ sb.append(cachedResult);
+ sb.append(endSixel());
+ return sb.toString();
+ }
+ // 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);
+ }
+ }
+ }
+
+ // 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);
+ }
+ image = palette.ditherImage(image);
+
+ // Collect the raster information
+ 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;
+ }
+ }
+ palette.emitPalette(sb, usedColors);
+ */
+
+ // Render the entire row of cells.
+ for (int currentRow = 0; currentRow < fullHeight; currentRow += 6) {
+ int [][] sixels = new int[image.getWidth()][6];
+
+ // See which colors are actually used in this band of sixels.
+ for (int imageX = 0; imageX < image.getWidth(); imageX++) {
+ for (int imageY = 0;
+ (imageY < 6) && (imageY + currentRow < fullHeight);
+ imageY++) {
+
+ int colorIdx = image.getRGB(imageX, imageY + currentRow);
+ assert (colorIdx >= 0);
+ assert (colorIdx < sixelPaletteSize);
+
+ sixels[imageX][imageY] = colorIdx;
+ }
+ }
+
+ for (int i = 0; i < sixelPaletteSize; i++) {
+ boolean isUsed = false;
+ for (int imageX = 0; imageX < image.getWidth(); imageX++) {
+ for (int j = 0; j < 6; j++) {
+ if (sixels[imageX][j] == i) {
+ isUsed = true;
+ }
+ }
+ }
+ if (isUsed == false) {
+ continue;
+ }
+
+ // Set to the beginning of scan line for the next set of
+ // colored pixels, and select the color.
+ sb.append(String.format("$#%d", i));
+
+ int oldData = -1;
+ int oldDataCount = 0;
+ for (int imageX = 0; imageX < image.getWidth(); imageX++) {
+
+ // Add up all the pixels that match this color.
+ int data = 0;
+ for (int j = 0;
+ (j < 6) && (currentRow + j < fullHeight);
+ j++) {
+
+ if (sixels[imageX][j] == i) {
+ switch (j) {
+ case 0:
+ data += 1;
+ break;
+ case 1:
+ data += 2;
+ break;
+ case 2:
+ data += 4;
+ break;
+ case 3:
+ data += 8;
+ break;
+ case 4:
+ data += 16;
+ break;
+ case 5:
+ data += 32;
+ break;
+ }
+ if ((currentRow + j + 1) > rasterHeight) {
+ rasterHeight = currentRow + j + 1;
+ }
+ }
+ }
+ assert (data >= 0);
+ assert (data < 64);
+ data += 63;
+
+ if (data == oldData) {
+ oldDataCount++;
+ } else {
+ if (oldDataCount == 1) {
+ sb.append((char) oldData);
+ } else if (oldDataCount > 1) {
+ sb.append(String.format("!%d", oldDataCount));
+ sb.append((char) oldData);
+ }
+ oldDataCount = 1;
+ oldData = data;
+ }
+
+ } // for (int imageX = 0; imageX < image.getWidth(); imageX++)
+
+ // Emit the last sequence.
+ if (oldDataCount == 1) {
+ sb.append((char) oldData);
+ } else if (oldDataCount > 1) {
+ sb.append(String.format("!%d", oldDataCount));
+ sb.append((char) oldData);
+ }
+
+ } // for (int i = 0; i < sixelPaletteSize; i++)
+
+ // Advance to the next scan line.
+ sb.append("-");
+
+ } // for (int currentRow = 0; currentRow < imageHeight; currentRow += 6)
+
+ // Kill the very last "-", because it is unnecessary.
+ sb.deleteCharAt(sb.length() - 1);
+
+ // Add the raster information
+ sb.insert(0, String.format("\"1;1;%d;%d", rasterWidth, rasterHeight));
+
+ if (saveInCache) {
+ // This row is OK to save into the cache.
+ sixelCache.put(cells, sb.toString());
+ }
+
+ return (startSixel(x, y) + sb.toString() + endSixel());
+ }
+
+ /**
+ * Get the sixel support flag.
+ *
+ * @return true if this terminal is emitting sixel
+ */
+ public boolean hasSixel() {
+ return sixel;
+ }
+
+ // ------------------------------------------------------------------------
+ // 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);
+ 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");
+ }
+
+ 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);
+ }
+ }
+ }
+
+ /*
+ * From https://iterm2.com/documentation-images.html:
+ *
+ * Protocol
+ *
+ * iTerm2 extends the xterm protocol with a set of proprietary escape
+ * sequences. In general, the pattern is:
+ *
+ * ESC ] 1337 ; key = value ^G
+ *
+ * Whitespace is shown here for ease of reading: in practice, no
+ * spaces should be used.
+ *
+ * For file transfer and inline images, the code is:
+ *
+ * ESC ] 1337 ; File = [optional arguments] : base-64 encoded file contents ^G
+ *
+ * The optional arguments are formatted as key=value with a semicolon
+ * between each key-value pair. They are described below:
+ *
+ * Key Description of value
+ * name base-64 encoded filename. Defaults to "Unnamed file".
+ * size File size in bytes. Optional; this is only used by the
+ * progress indicator.
+ * width Width to render. See notes below.
+ * height Height to render. See notes below.
+ * preserveAspectRatio If set to 0, then the image's inherent aspect
+ * ratio will not be respected; otherwise, it
+ * will fill the specified width and height as
+ * much as possible without stretching. Defaults
+ * to 1.
+ * inline If set to 1, the file will be displayed inline. Otherwise,
+ * it will be downloaded with no visual representation in the
+ * terminal session. Defaults to 0.
+ *
+ * The width and height are given as a number followed by a unit, or
+ * the word "auto".
+ *
+ * N: N character cells.
+ * Npx: N pixels.
+ * N%: N percent of the session's width or height.
+ * auto: The image's inherent size will be used to determine an
+ * appropriate dimension.
+ *
+ */
+
+ // File contents can be several image formats. We will use 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 "";
+ }
+
+ // 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;",
+ cells.size()));
+ */
+ /*
+ sb.append(String.format("width=$dpx;height=%dpx;preserveAspectRatio=1;",
+ image.getWidth(), Math.min(image.getHeight(),
+ getTextHeight())));
+ */
+ sb.append("inline=1:");
+ sb.append(base64.encodeToString(pngOutputStream.toByteArray()));
+ sb.append("\007");
+
+ if (saveInCache) {
+ // This row is OK to save into the cache.
+ iterm2Cache.put(cells, sb.toString());
+ }
+
+ return (gotoXY(x, y) + sb.toString());
+ }
+
+ /**
+ * Get the iTerm2 images support flag.
+ *
+ * @return true if this terminal is emitting iTerm2 images
+ */
+ public boolean hasIterm2Images() {
+ return iterm2Images;
+ }
+
+ // ------------------------------------------------------------------------
+ // End iTerm2 image output support ----------------------------------------
+ // ------------------------------------------------------------------------
+
+ // ------------------------------------------------------------------------
+ // Jexer image output support ---------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Create a Jexer 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 toJexerImage(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 (jexerImages == false) {
+ sb.append(normal());
+ sb.append(gotoXY(x, y));
+ for (int i = 0; i < cells.size(); i++) {
+ sb.append(' ');
+ }
+ return sb.toString();
+ }
+
+ 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
+ // cells.
+ boolean saveInCache = true;
+ for (Cell cell: cells) {
+ if (cell.isInvertedImage()) {
+ saveInCache = false;
+ }
+ }
+ if (saveInCache) {
+ String cachedResult = jexerCache.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");
+ }
+
+ 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);
+ }
+ }
+ }
+
+ 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);
+ }
+ }
+ sb.append(base64.encodeToString(bytes));
+ sb.append("\007");
+
+ if (saveInCache) {
+ // This row is OK to save into the cache.
+ jexerCache.put(cells, sb.toString());
+ }
+
+ return (gotoXY(x, y) + sb.toString());
+ }
+
+ /**
+ * Get the Jexer images support flag.
+ *
+ * @return true if this terminal is emitting Jexer images
+ */
+ public boolean hasJexerImages() {
+ return jexerImages;
+ }
+
+ // ------------------------------------------------------------------------
+ // End Jexer image output support -----------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Setup system colors to match DOS color palette.
+ */
+ private void setDOSColors() {
+ MYBLACK = new java.awt.Color(0x00, 0x00, 0x00);
+ MYRED = new java.awt.Color(0xa8, 0x00, 0x00);
+ MYGREEN = new java.awt.Color(0x00, 0xa8, 0x00);
+ MYYELLOW = new java.awt.Color(0xa8, 0x54, 0x00);
+ MYBLUE = new java.awt.Color(0x00, 0x00, 0xa8);
+ MYMAGENTA = new java.awt.Color(0xa8, 0x00, 0xa8);
+ MYCYAN = new java.awt.Color(0x00, 0xa8, 0xa8);
+ MYWHITE = new java.awt.Color(0xa8, 0xa8, 0xa8);
+ MYBOLD_BLACK = new java.awt.Color(0x54, 0x54, 0x54);
+ MYBOLD_RED = new java.awt.Color(0xfc, 0x54, 0x54);
+ MYBOLD_GREEN = new java.awt.Color(0x54, 0xfc, 0x54);
+ MYBOLD_YELLOW = new java.awt.Color(0xfc, 0xfc, 0x54);
+ MYBOLD_BLUE = new java.awt.Color(0x54, 0x54, 0xfc);
+ MYBOLD_MAGENTA = new java.awt.Color(0xfc, 0x54, 0xfc);
+ MYBOLD_CYAN = new java.awt.Color(0x54, 0xfc, 0xfc);
+ MYBOLD_WHITE = new java.awt.Color(0xfc, 0xfc, 0xfc);
+ }
+
+ /**
+ * Setup ECMA48 colors to match those provided in system properties.
+ */
+ private void setCustomSystemColors() {
+ setDOSColors();
+
+ MYBLACK = getCustomColor("jexer.ECMA48.color0", MYBLACK);
+ MYRED = getCustomColor("jexer.ECMA48.color1", MYRED);
+ MYGREEN = getCustomColor("jexer.ECMA48.color2", MYGREEN);
+ MYYELLOW = getCustomColor("jexer.ECMA48.color3", MYYELLOW);
+ MYBLUE = getCustomColor("jexer.ECMA48.color4", MYBLUE);
+ MYMAGENTA = getCustomColor("jexer.ECMA48.color5", MYMAGENTA);
+ MYCYAN = getCustomColor("jexer.ECMA48.color6", MYCYAN);
+ MYWHITE = getCustomColor("jexer.ECMA48.color7", MYWHITE);
+ MYBOLD_BLACK = getCustomColor("jexer.ECMA48.color8", MYBOLD_BLACK);
+ MYBOLD_RED = getCustomColor("jexer.ECMA48.color9", MYBOLD_RED);
+ MYBOLD_GREEN = getCustomColor("jexer.ECMA48.color10", MYBOLD_GREEN);
+ MYBOLD_YELLOW = getCustomColor("jexer.ECMA48.color11", MYBOLD_YELLOW);
+ MYBOLD_BLUE = getCustomColor("jexer.ECMA48.color12", MYBOLD_BLUE);
+ MYBOLD_MAGENTA = getCustomColor("jexer.ECMA48.color13", MYBOLD_MAGENTA);
+ MYBOLD_CYAN = getCustomColor("jexer.ECMA48.color14", MYBOLD_CYAN);
+ MYBOLD_WHITE = getCustomColor("jexer.ECMA48.color15", MYBOLD_WHITE);
+ }
+
+ /**
+ * Setup one system color to match the RGB value provided in system
+ * properties.
+ *
+ * @param key the system property key
+ * @param defaultColor the default color to return if key is not set, or
+ * incorrect
+ * @return a color from the RGB string, or defaultColor
+ */
+ private java.awt.Color getCustomColor(final String key,
+ final java.awt.Color defaultColor) {
+
+ String rgb = System.getProperty(key);
+ if (rgb == null) {
+ return defaultColor;
+ }
+ if (rgb.startsWith("#")) {
+ rgb = rgb.substring(1);
+ }
+ int rgbInt = 0;
+ try {
+ rgbInt = Integer.parseInt(rgb, 16);
+ } catch (NumberFormatException e) {
+ return defaultColor;
+ }
+ java.awt.Color color = new java.awt.Color((rgbInt & 0xFF0000) >>> 16,
+ (rgbInt & 0x00FF00) >>> 8,
+ (rgbInt & 0x0000FF));
+
+ return color;
+ }
+
+ /**
+ * Create a T.416 RGB parameter sequence for a custom system color.
+ *
+ * @param color one of the MYBLACK, MYBOLD_BLUE, etc. colors
+ * @return the color portion of the string to emit to an ANSI /
+ * ECMA-style terminal
+ */
+ private String systemColorRGB(final java.awt.Color color) {
+ return String.format("%d;%d;%d", color.getRed(), color.getGreen(),
+ color.getBlue());
+ }
+
+ /**
+ * Create a SGR parameter sequence for a single color change.
+ *
+ * @param bold if true, set bold
+ * @param color one of the Color.WHITE, Color.BLUE, etc. constants
+ * @param foreground if true, this is a foreground color
+ * @return the string to emit to an ANSI / ECMA-style terminal,
+ * e.g. "\033[42m"
+ */
+ private String color(final boolean bold, final Color color,
+ final boolean foreground) {
+ return color(color, foreground, true) +
+ rgbColor(bold, color, foreground);
+ }
+
+ /**
+ * Create a T.416 RGB parameter sequence for a single color change.
+ *
+ * @param colorRGB a 24-bit RGB value for foreground color
+ * @param foreground if true, this is a foreground color
+ * @return the string to emit to an ANSI / ECMA-style terminal,
+ * e.g. "\033[42m"
+ */
+ private String colorRGB(final int colorRGB, final boolean foreground) {
+
+ int colorRed = (colorRGB >>> 16) & 0xFF;
+ int colorGreen = (colorRGB >>> 8) & 0xFF;
+ int colorBlue = colorRGB & 0xFF;
+
+ StringBuilder sb = new StringBuilder();
+ if (foreground) {
+ sb.append("\033[38;2;");
+ } else {
+ sb.append("\033[48;2;");
+ }
+ sb.append(String.format("%d;%d;%dm", colorRed, colorGreen, colorBlue));
+ return sb.toString();
+ }
+
+ /**
+ * Create a T.416 RGB parameter sequence for both foreground and
+ * background color change.
+ *
+ * @param foreColorRGB a 24-bit RGB value for foreground color
+ * @param backColorRGB a 24-bit RGB value for foreground color
+ * @return the string to emit to an ANSI / ECMA-style terminal,
+ * e.g. "\033[42m"
+ */
+ private String colorRGB(final int foreColorRGB, final int backColorRGB) {
+ int foreColorRed = (foreColorRGB >>> 16) & 0xFF;
+ int foreColorGreen = (foreColorRGB >>> 8) & 0xFF;
+ int foreColorBlue = foreColorRGB & 0xFF;
+ int backColorRed = (backColorRGB >>> 16) & 0xFF;
+ int backColorGreen = (backColorRGB >>> 8) & 0xFF;
+ int backColorBlue = backColorRGB & 0xFF;
+
+ StringBuilder sb = new StringBuilder();
+ sb.append(String.format("\033[38;2;%d;%d;%dm",
+ foreColorRed, foreColorGreen, foreColorBlue));
+ sb.append(String.format("\033[48;2;%d;%d;%dm",
+ backColorRed, backColorGreen, backColorBlue));
+ return sb.toString();
+ }
+
+ /**
+ * Create a T.416 RGB parameter sequence for a single color change.
+ *
+ * @param bold if true, set bold
+ * @param color one of the Color.WHITE, Color.BLUE, etc. constants
+ * @param foreground if true, this is a foreground color
+ * @return the string to emit to an xterm terminal with RGB support,
+ * e.g. "\033[38;2;RR;GG;BBm"
+ */
+ private String rgbColor(final boolean bold, final Color color,
+ final boolean foreground) {
+ if (doRgbColor == false) {
+ return "";
+ }
+ StringBuilder sb = new StringBuilder("\033[");
+ if (bold) {
+ // Bold implies foreground only
+ sb.append("38;2;");
+ if (color.equals(Color.BLACK)) {
+ sb.append(systemColorRGB(MYBOLD_BLACK));
+ } else if (color.equals(Color.RED)) {
+ sb.append(systemColorRGB(MYBOLD_RED));
+ } else if (color.equals(Color.GREEN)) {
+ sb.append(systemColorRGB(MYBOLD_GREEN));
+ } else if (color.equals(Color.YELLOW)) {
+ sb.append(systemColorRGB(MYBOLD_YELLOW));
+ } else if (color.equals(Color.BLUE)) {
+ sb.append(systemColorRGB(MYBOLD_BLUE));
+ } else if (color.equals(Color.MAGENTA)) {
+ sb.append(systemColorRGB(MYBOLD_MAGENTA));
+ } else if (color.equals(Color.CYAN)) {
+ sb.append(systemColorRGB(MYBOLD_CYAN));
+ } else if (color.equals(Color.WHITE)) {
+ sb.append(systemColorRGB(MYBOLD_WHITE));
+ }
+ } else {
+ if (foreground) {
+ sb.append("38;2;");
+ } else {
+ sb.append("48;2;");
+ }
+ if (color.equals(Color.BLACK)) {
+ sb.append(systemColorRGB(MYBLACK));
+ } else if (color.equals(Color.RED)) {
+ sb.append(systemColorRGB(MYRED));
+ } else if (color.equals(Color.GREEN)) {
+ sb.append(systemColorRGB(MYGREEN));
+ } else if (color.equals(Color.YELLOW)) {
+ sb.append(systemColorRGB(MYYELLOW));
+ } else if (color.equals(Color.BLUE)) {
+ sb.append(systemColorRGB(MYBLUE));
+ } else if (color.equals(Color.MAGENTA)) {
+ sb.append(systemColorRGB(MYMAGENTA));
+ } else if (color.equals(Color.CYAN)) {
+ sb.append(systemColorRGB(MYCYAN));
+ } else if (color.equals(Color.WHITE)) {
+ sb.append(systemColorRGB(MYWHITE));
+ }
+ }
+ sb.append("m");
+ return sb.toString();
+ }
+
+ /**
+ * Create a T.416 RGB parameter sequence for both foreground and
+ * background color change.
+ *
+ * @param bold if true, set bold
+ * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
+ * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
+ * @return the string to emit to an xterm terminal with RGB support,
+ * e.g. "\033[38;2;RR;GG;BB;48;2;RR;GG;BBm"
+ */
+ private String rgbColor(final boolean bold, final Color foreColor,
+ final Color backColor) {
+ if (doRgbColor == false) {
+ return "";
+ }
+
+ return rgbColor(bold, foreColor, true) +
+ rgbColor(false, backColor, false);
+ }
+
+ /**
+ * Create a SGR parameter sequence for a single color change.
+ *
+ * @param color one of the Color.WHITE, Color.BLUE, etc. constants
+ * @param foreground if true, this is a foreground color
+ * @param header if true, make the full header, otherwise just emit the
+ * color parameter e.g. "42;"
+ * @return the string to emit to an ANSI / ECMA-style terminal,
+ * e.g. "\033[42m"
+ */
+ private String color(final Color color, final boolean foreground,
+ final boolean header) {
+
+ int ecmaColor = color.getValue();
+
+ // Convert Color.* values to SGR numerics
+ if (foreground) {
+ ecmaColor += 30;
+ } else {
+ ecmaColor += 40;
+ }
+
+ if (header) {
+ return String.format("\033[%dm", ecmaColor);
+ } else {
+ return String.format("%d;", ecmaColor);
+ }
+ }
+
+ /**
+ * Create a SGR parameter sequence for both foreground and background
+ * color change.
+ *
+ * @param bold if true, set bold
+ * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
+ * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
+ * @return the string to emit to an ANSI / ECMA-style terminal,
+ * e.g. "\033[31;42m"
+ */
+ private String color(final boolean bold, final Color foreColor,
+ final Color backColor) {
+ return color(foreColor, backColor, true) +
+ rgbColor(bold, foreColor, backColor);
+ }
+
+ /**
+ * Create a SGR parameter sequence for both foreground and
+ * background color change.
+ *
+ * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
+ * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
+ * @param header if true, make the full header, otherwise just emit the
+ * color parameter e.g. "31;42;"
+ * @return the string to emit to an ANSI / ECMA-style terminal,
+ * e.g. "\033[31;42m"
+ */
+ private String color(final Color foreColor, final Color backColor,
+ final boolean header) {
+
+ int ecmaForeColor = foreColor.getValue();
+ int ecmaBackColor = backColor.getValue();
+
+ // Convert Color.* values to SGR numerics
+ ecmaBackColor += 40;
+ ecmaForeColor += 30;
+
+ if (header) {
+ return String.format("\033[%d;%dm", ecmaForeColor, ecmaBackColor);
+ } else {
+ return String.format("%d;%d;", ecmaForeColor, ecmaBackColor);
+ }
+ }
+
+ /**
+ * Create a SGR parameter sequence for foreground, background, and
+ * several attributes. This sequence first resets all attributes to
+ * default, then sets attributes as per the parameters.
+ *
+ * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
+ * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
+ * @param bold if true, set bold
+ * @param reverse if true, set reverse
+ * @param blink if true, set blink
+ * @param underline if true, set underline
+ * @return the string to emit to an ANSI / ECMA-style terminal,
+ * e.g. "\033[0;1;31;42m"
+ */
+ private String color(final Color foreColor, final Color backColor,
+ final boolean bold, final boolean reverse, final boolean blink,
+ final boolean underline) {
+
+ int ecmaForeColor = foreColor.getValue();
+ int ecmaBackColor = backColor.getValue();
+
+ // Convert Color.* values to SGR numerics
+ ecmaBackColor += 40;
+ ecmaForeColor += 30;
+
+ StringBuilder sb = new StringBuilder();
+ if ( bold && reverse && blink && !underline ) {
+ sb.append("\033[0;1;7;5;");
+ } else if ( bold && reverse && !blink && !underline ) {
+ sb.append("\033[0;1;7;");
+ } else if ( !bold && reverse && blink && !underline ) {
+ sb.append("\033[0;7;5;");
+ } else if ( bold && !reverse && blink && !underline ) {
+ sb.append("\033[0;1;5;");
+ } else if ( bold && !reverse && !blink && !underline ) {
+ sb.append("\033[0;1;");
+ } else if ( !bold && reverse && !blink && !underline ) {
+ sb.append("\033[0;7;");
+ } else if ( !bold && !reverse && blink && !underline) {
+ sb.append("\033[0;5;");
+ } else if ( bold && reverse && blink && underline ) {
+ sb.append("\033[0;1;7;5;4;");
+ } else if ( bold && reverse && !blink && underline ) {
+ sb.append("\033[0;1;7;4;");
+ } else if ( !bold && reverse && blink && underline ) {
+ sb.append("\033[0;7;5;4;");
+ } else if ( bold && !reverse && blink && underline ) {
+ sb.append("\033[0;1;5;4;");
+ } else if ( bold && !reverse && !blink && underline ) {
+ sb.append("\033[0;1;4;");
+ } else if ( !bold && reverse && !blink && underline ) {
+ sb.append("\033[0;7;4;");
+ } else if ( !bold && !reverse && blink && underline) {
+ sb.append("\033[0;5;4;");
+ } else if ( !bold && !reverse && !blink && underline) {
+ sb.append("\033[0;4;");
+ } else {
+ assert (!bold && !reverse && !blink && !underline);
+ sb.append("\033[0;");
+ }
+ sb.append(String.format("%d;%dm", ecmaForeColor, ecmaBackColor));
+ sb.append(rgbColor(bold, foreColor, backColor));
+ return sb.toString();
+ }
+
+ /**
+ * Create a SGR parameter sequence for foreground, background, and
+ * several attributes. This sequence first resets all attributes to
+ * default, then sets attributes as per the parameters.
+ *
+ * @param foreColorRGB a 24-bit RGB value for foreground color
+ * @param backColorRGB a 24-bit RGB value for foreground color
+ * @param bold if true, set bold
+ * @param reverse if true, set reverse
+ * @param blink if true, set blink
+ * @param underline if true, set underline
+ * @return the string to emit to an ANSI / ECMA-style terminal,
+ * e.g. "\033[0;1;31;42m"
+ */
+ private String colorRGB(final int foreColorRGB, final int backColorRGB,
+ final boolean bold, final boolean reverse, final boolean blink,
+ final boolean underline) {
+
+ int foreColorRed = (foreColorRGB >>> 16) & 0xFF;
+ int foreColorGreen = (foreColorRGB >>> 8) & 0xFF;
+ int foreColorBlue = foreColorRGB & 0xFF;
+ int backColorRed = (backColorRGB >>> 16) & 0xFF;
+ int backColorGreen = (backColorRGB >>> 8) & 0xFF;
+ int backColorBlue = backColorRGB & 0xFF;
+
+ StringBuilder sb = new StringBuilder();
+ if ( bold && reverse && blink && !underline ) {
+ sb.append("\033[0;1;7;5;");
+ } else if ( bold && reverse && !blink && !underline ) {
+ sb.append("\033[0;1;7;");
+ } else if ( !bold && reverse && blink && !underline ) {
+ sb.append("\033[0;7;5;");
+ } else if ( bold && !reverse && blink && !underline ) {
+ sb.append("\033[0;1;5;");
+ } else if ( bold && !reverse && !blink && !underline ) {
+ sb.append("\033[0;1;");
+ } else if ( !bold && reverse && !blink && !underline ) {
+ sb.append("\033[0;7;");
+ } else if ( !bold && !reverse && blink && !underline) {
+ sb.append("\033[0;5;");
+ } else if ( bold && reverse && blink && underline ) {
+ sb.append("\033[0;1;7;5;4;");
+ } else if ( bold && reverse && !blink && underline ) {
+ sb.append("\033[0;1;7;4;");
+ } else if ( !bold && reverse && blink && underline ) {
+ sb.append("\033[0;7;5;4;");
+ } else if ( bold && !reverse && blink && underline ) {
+ sb.append("\033[0;1;5;4;");
+ } else if ( bold && !reverse && !blink && underline ) {
+ sb.append("\033[0;1;4;");
+ } else if ( !bold && reverse && !blink && underline ) {
+ sb.append("\033[0;7;4;");
+ } else if ( !bold && !reverse && blink && underline) {
+ sb.append("\033[0;5;4;");
+ } else if ( !bold && !reverse && !blink && underline) {
+ sb.append("\033[0;4;");
+ } else {
+ assert (!bold && !reverse && !blink && !underline);
+ sb.append("\033[0;");
+ }
+
+ sb.append("m\033[38;2;");
+ sb.append(String.format("%d;%d;%d", foreColorRed, foreColorGreen,
+ foreColorBlue));
+ sb.append("m\033[48;2;");
+ sb.append(String.format("%d;%d;%d", backColorRed, backColorGreen,
+ backColorBlue));
+ sb.append("m");
+ return sb.toString();
+ }
+
+ /**
+ * Create a SGR parameter sequence to reset to VT100 defaults.
+ *
+ * @return the string to emit to an ANSI / ECMA-style terminal,
+ * e.g. "\033[0m"
+ */
+ private String normal() {
+ return normal(true) + rgbColor(false, Color.WHITE, Color.BLACK);
+ }
+
+ /**
+ * Create a SGR parameter sequence to reset to ECMA-48 default
+ * foreground/background.
+ *
+ * @return the string to emit to an ANSI / ECMA-style terminal,
+ * e.g. "\033[0m"
+ */
+ private String defaultColor() {
+ /*
+ * VT100 normal.
+ * Normal (neither bold nor faint).
+ * Not italicized.
+ * Not underlined.
+ * Steady (not blinking).
+ * Positive (not inverse).
+ * Visible (not hidden).
+ * Not crossed-out.
+ * Default foreground color.
+ * Default background color.
+ */
+ return "\033[0;22;23;24;25;27;28;29;39;49m";
+ }
+
+ /**
+ * Create a SGR parameter sequence to reset to defaults.
+ *
+ * @param header if true, make the full header, otherwise just emit the
+ * bare parameter e.g. "0;"
+ * @return the string to emit to an ANSI / ECMA-style terminal,
+ * e.g. "\033[0m"
+ */
+ private String normal(final boolean header) {
+ if (header) {
+ return "\033[0;37;40m";
+ }
+ return "0;37;40";
+ }
+
+ /**
+ * Create a SGR parameter sequence for enabling the visible cursor.
+ *
+ * @param on if true, turn on cursor
+ * @return the string to emit to an ANSI / ECMA-style terminal
+ */
+ private String cursor(final boolean on) {
+ if (on && !cursorOn) {
+ cursorOn = true;
+ return "\033[?25h";
+ }
+ if (!on && cursorOn) {
+ cursorOn = false;
+ return "\033[?25l";
+ }
+ return "";
+ }
+
+ /**
+ * Clear the entire screen. Because some terminals use back-color-erase,
+ * set the color to white-on-black beforehand.
+ *
+ * @return the string to emit to an ANSI / ECMA-style terminal
+ */
+ private String clearAll() {
+ return "\033[0;37;40m\033[2J";
+ }
+
+ /**
+ * Clear the line from the cursor (inclusive) to the end of the screen.
+ * Because some terminals use back-color-erase, set the color to
+ * white-on-black beforehand.
+ *
+ * @return the string to emit to an ANSI / ECMA-style terminal
+ */
+ private String clearRemainingLine() {
+ return "\033[0;37;40m\033[K";
+ }
+
+ /**
+ * Move the cursor to (x, y).
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @return the string to emit to an ANSI / ECMA-style terminal
+ */
+ private String gotoXY(final int x, final int y) {
+ return String.format("\033[%d;%dH", y + 1, x + 1);
+ }
+
+ /**
+ * Tell (u)xterm that we want to receive mouse events based on "Any event
+ * tracking", UTF-8 coordinates, and then SGR coordinates. Ideally we
+ * will end up with SGR coordinates with UTF-8 coordinates as a fallback.
+ * See
+ * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
+ *
+ * Note that this also sets the alternate/primary screen buffer.
+ *
+ * Finally, also emit a Privacy Message sequence that Jexer recognizes to
+ * mean "hide the mouse pointer." We have to use our own sequence to do
+ * this because there is no standard in xterm for unilaterally hiding the
+ * pointer all the time (regardless of typing).
+ *
+ * @param on If true, enable mouse report and use the alternate screen
+ * buffer. If false disable mouse reporting and use the primary screen
+ * buffer.
+ * @return the string to emit to xterm
+ */
+ private String mouse(final boolean on) {
+ if (on) {
+ return "\033[?1002;1003;1005;1006h\033[?1049h\033^hideMousePointer\033\\";
+ }
+ return "\033[?1002;1003;1006;1005l\033[?1049l\033^showMousePointer\033\\";
+ }
+
+ }
--- /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;
+ import jexer.event.TCommandEvent;
+ import static jexer.TCommand.*;
+
+ /**
+ * This abstract class provides a screen, keyboard, and mouse to
+ * TApplication. It also exposes session information as gleaned from lower
+ * levels of the communication stack.
+ */
+ public abstract class GenericBackend implements Backend {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The session information.
+ */
+ protected SessionInfo sessionInfo;
+
+ /**
+ * The screen to draw on.
+ */
+ protected Screen screen;
+
+ /**
+ * Input events are processed by this Terminal.
+ */
+ protected TerminalReader terminal;
+
+ /**
+ * By default, GenericBackend adds a cmAbort after it sees
+ * cmBackendDisconnect, so that TApplication will exit when the user
+ * closes the Swing window or disconnects the ECMA48 streams. But
+ * MultiBackend wraps multiple Backends, and needs to decide when to send
+ * cmAbort differently. Setting this to false is how it manages that.
+ * Note package private access.
+ */
+ boolean abortOnDisconnect = true;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ // ------------------------------------------------------------------------
+ // Backend ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Getter for sessionInfo.
+ *
+ * @return the SessionInfo
+ */
+ public final SessionInfo getSessionInfo() {
+ return sessionInfo;
+ }
+
+ /**
+ * Getter for screen.
+ *
+ * @return the Screen
+ */
+ public final Screen getScreen() {
+ return screen;
+ }
+
+ /**
+ * Sync the logical screen to the physical device.
+ */
+ public void flushScreen() {
+ screen.flushPhysical();
+ }
+
+ /**
+ * Check if there are events in the queue.
+ *
+ * @return if true, getEvents() has something to return to the application
+ */
+ public boolean hasEvents() {
+ return terminal.hasEvents();
+ }
+
+ /**
+ * Get keyboard, mouse, and screen resize events.
+ *
+ * @param queue list to append new events to
+ */
+ public void getEvents(final List<TInputEvent> queue) {
+ if (terminal.hasEvents()) {
+ terminal.getEvents(queue);
+
+ // This default backend assumes a single user, and if that user
+ // becomes disconnected we should terminate the application.
+ if ((queue.size() > 0) && (abortOnDisconnect == true)) {
+ TInputEvent event = queue.get(queue.size() - 1);
+ if (event instanceof TCommandEvent) {
+ TCommandEvent command = (TCommandEvent) event;
+ if (command.equals(cmBackendDisconnect)) {
+ queue.add(new TCommandEvent(cmAbort));
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Close the I/O, restore the console, etc.
+ */
+ public void shutdown() {
+ terminal.closeTerminal();
+ }
+
+ /**
+ * Set the window title.
+ *
+ * @param title the new title
+ */
+ public void setTitle(final String title) {
+ screen.setTitle(title);
+ }
+
+ /**
+ * 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) {
+ terminal.setListener(listener);
+ }
+
+ /**
+ * Reload backend options from System properties.
+ */
+ public void reloadOptions() {
+ terminal.reloadOptions();
+ }
+
+ }
--- /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.awt.Font;
+ import java.awt.FontMetrics;
+ import java.awt.Graphics2D;
+ import java.awt.geom.Rectangle2D;
+ import java.awt.image.BufferedImage;
+ import java.io.InputStream;
+ import java.io.IOException;
+ import java.util.HashMap;
+
+ import jexer.bits.Cell;
+ import jexer.bits.StringUtils;
+
+ /**
+ * GlyphMakerFont creates glyphs as bitmaps from a font.
+ */
+ class GlyphMakerFont {
+
+ // ------------------------------------------------------------------------
+ // Constants --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * If true, enable debug messages.
+ */
+ private static boolean DEBUG = false;
+
+ /**
+ * If true, we were successful at getting the font dimensions.
+ */
+ private boolean gotFontDimensions = false;
+
+ /**
+ * The currently selected font.
+ */
+ private Font font = null;
+
+ /**
+ * Width of a character cell in pixels.
+ */
+ private int textWidth = 1;
+
+ /**
+ * Height of a character cell in pixels.
+ */
+ private int textHeight = 1;
+
+ /**
+ * Width of a character cell in pixels, as reported by font.
+ */
+ private int fontTextWidth = 1;
+
+ /**
+ * Height of a character cell in pixels, as reported by font.
+ */
+ private int fontTextHeight = 1;
+
+ /**
+ * Descent of a character cell in pixels.
+ */
+ private int maxDescent = 0;
+
+ /**
+ * System-dependent Y adjustment for text in the character cell.
+ */
+ private int textAdjustY = 0;
+
+ /**
+ * System-dependent X adjustment for text in the character cell.
+ */
+ private int textAdjustX = 0;
+
+ /**
+ * System-dependent height adjustment for text in the character cell.
+ */
+ private int textAdjustHeight = 0;
+
+ /**
+ * System-dependent width adjustment for text in the character cell.
+ */
+ private int textAdjustWidth = 0;
+
+ /**
+ * A cache of previously-rendered glyphs for blinking text, when it is
+ * not visible.
+ */
+ private HashMap<Cell, BufferedImage> glyphCacheBlink;
+
+ /**
+ * A cache of previously-rendered glyphs for non-blinking, or
+ * blinking-and-visible, text.
+ */
+ private HashMap<Cell, BufferedImage> glyphCache;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param filename the resource filename of the font to use
+ * @param fontSize the size of font to use
+ */
+ public GlyphMakerFont(final String filename, final int fontSize) {
+
+ if (filename.length() == 0) {
+ // Fallback font
+ font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize);
+ return;
+ }
+
+ Font fontRoot = null;
+ try {
+ 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) {
+ // 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) {
+ // See comment above.
+ font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize);
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // GlyphMakerFont ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get a glyph image.
+ *
+ * @param cell the character to draw
+ * @param cellWidth the width of the text cell to draw into
+ * @param cellHeight the height of the text cell to draw into
+ * @return the glyph as an image
+ */
+ public BufferedImage getImage(final Cell cell, final int cellWidth,
+ final int cellHeight) {
+
+ return getImage(cell, cellWidth, cellHeight, true);
+ }
+
+ /**
+ * Get a glyph image.
+ *
+ * @param cell the character to draw
+ * @param cellWidth the width of the text cell to draw into
+ * @param cellHeight the height of the text cell to draw into
+ * @param blinkVisible if true, the cell is visible if it is blinking
+ * @return the glyph as an image
+ */
+ public BufferedImage getImage(final Cell cell, final int cellWidth,
+ final int cellHeight, final boolean blinkVisible) {
+
+ if (gotFontDimensions == false) {
+ // Lazy-load the text width/height and adjustments.
+ getFontDimensions();
+ }
+
+ if (DEBUG && !font.canDisplay(cell.getChar())) {
+ System.err.println("font " + font + " has no glyph for " +
+ String.format("0x%x", cell.getChar()));
+ }
+
+ BufferedImage image = null;
+ if (cell.isBlink() && !blinkVisible) {
+ image = glyphCacheBlink.get(cell);
+ } else {
+ image = glyphCache.get(cell);
+ }
+ if (image != null) {
+ return image;
+ }
+
+ // Generate glyph and draw it.
+ image = new BufferedImage(cellWidth, cellHeight,
+ BufferedImage.TYPE_INT_ARGB);
+ Graphics2D gr2 = image.createGraphics();
+ gr2.setFont(font);
+
+ Cell cellColor = new Cell(cell);
+
+ // Check for reverse
+ if (cell.isReverse()) {
+ cellColor.setForeColor(cell.getBackColor());
+ cellColor.setBackColor(cell.getForeColor());
+ }
+
+ // Draw the background rectangle, then the foreground character.
+ gr2.setColor(SwingTerminal.attrToBackgroundColor(cellColor));
+ gr2.fillRect(0, 0, cellWidth, cellHeight);
+
+ // Handle blink and underline
+ if (!cell.isBlink()
+ || (cell.isBlink() && blinkVisible)
+ ) {
+ gr2.setColor(SwingTerminal.attrToForegroundColor(cellColor));
+ char [] chars = Character.toChars(cell.getChar());
+ gr2.drawChars(chars, 0, chars.length, textAdjustX,
+ cellHeight - maxDescent + textAdjustY);
+
+ if (cell.isUnderline()) {
+ gr2.fillRect(0, cellHeight - 2, cellWidth, 2);
+ }
+ }
+ gr2.dispose();
+
+ // We need a new key that will not be mutated by invertCell().
+ Cell key = new Cell(cell);
+ if (cell.isBlink() && !blinkVisible) {
+ glyphCacheBlink.put(key, image);
+ } else {
+ glyphCache.put(key, image);
+ }
+
+ /*
+ System.err.println("cellWidth " + cellWidth +
+ " cellHeight " + cellHeight + " image " + image);
+ */
+
+ return image;
+ }
+
+ /**
+ * Figure out my font dimensions.
+ */
+ private void getFontDimensions() {
+ glyphCacheBlink = new HashMap<Cell, BufferedImage>();
+ glyphCache = new HashMap<Cell, BufferedImage>();
+
+ BufferedImage image = new BufferedImage(font.getSize() * 2,
+ font.getSize() * 2, BufferedImage.TYPE_INT_ARGB);
+ Graphics2D gr = image.createGraphics();
+ gr.setFont(font);
+ FontMetrics fm = gr.getFontMetrics();
+ maxDescent = fm.getMaxDescent();
+ Rectangle2D bounds = fm.getMaxCharBounds(gr);
+ int leading = fm.getLeading();
+ fontTextWidth = (int)Math.round(bounds.getWidth());
+ // fontTextHeight = (int)Math.round(bounds.getHeight()) - maxDescent;
+
+ // This produces the same number, but works better for ugly
+ // monospace.
+ fontTextHeight = fm.getMaxAscent() + maxDescent - leading;
+ gr.dispose();
+
+ textHeight = fontTextHeight + textAdjustHeight;
+ textWidth = fontTextWidth + textAdjustWidth;
+ /*
+ System.err.println("font " + font);
+ System.err.println("fontTextWidth " + fontTextWidth);
+ System.err.println("fontTextHeight " + fontTextHeight);
+ System.err.println("textWidth " + textWidth);
+ System.err.println("textHeight " + textHeight);
+ */
+
+ gotFontDimensions = true;
+ }
+
+ /**
+ * Checks if this maker's Font has a glyph for the specified character.
+ *
+ * @param codePoint the character (Unicode code point) for which a glyph
+ * is needed.
+ * @return true if this Font has a glyph for the character; false
+ * otherwise.
+ */
+ public boolean canDisplay(final int codePoint) {
+ return font.canDisplay(codePoint);
+ }
+ }
+
+ /**
+ * GlyphMaker presents unified interface to all of its supported fonts to
+ * clients.
+ */
+ public class GlyphMaker {
+
+ // ------------------------------------------------------------------------
+ // Constants --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The mono font resource filename (terminus).
+ */
+ private static final String MONO = "terminus-ttf-4.39/TerminusTTF-Bold-4.39.ttf";
+
+ /**
+ * The CJK font resource filename.
+ */
+ private static final String cjkFontFilename = "NotoSansMonoCJKtc-Regular.otf";
+
+ /**
+ * The emoji font resource filename.
+ */
+ private static final String emojiFontFilename = "OpenSansEmoji.ttf";
+
+ /**
+ * The fallback font resource filename.
+ */
+ private static final String fallbackFontFilename = "";
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * If true, enable debug messages.
+ */
+ private static boolean DEBUG = false;
+
+ /**
+ * Cache of font bundles by size.
+ */
+ private static HashMap<Integer, GlyphMaker> makers = new HashMap<Integer, GlyphMaker>();
+
+ /**
+ * The instance that has the mono (default) font.
+ */
+ private GlyphMakerFont makerMono;
+
+ /**
+ * The instance that has the CJK font.
+ */
+ private GlyphMakerFont makerCjk;
+
+ /**
+ * The instance that has the emoji font.
+ */
+ private GlyphMakerFont makerEmoji;
+
+ /**
+ * The instance that has the fallback font.
+ */
+ private GlyphMakerFont makerFallback;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Create an instance with references to the necessary fonts.
+ *
+ * @param fontSize the size of these fonts in pixels
+ */
+ private GlyphMaker(final int fontSize) {
+ makerMono = new GlyphMakerFont(MONO, fontSize);
+
+ String fontFilename = null;
+ fontFilename = System.getProperty("jexer.cjkFont.filename",
+ cjkFontFilename);
+ makerCjk = new GlyphMakerFont(fontFilename, fontSize);
+ fontFilename = System.getProperty("jexer.emojiFont.filename",
+ emojiFontFilename);
+ makerEmoji = new GlyphMakerFont(fontFilename, fontSize);
+ fontFilename = System.getProperty("jexer.fallbackFont.filename",
+ fallbackFontFilename);
+ makerFallback = new GlyphMakerFont(fontFilename, fontSize);
+ }
+
+ // ------------------------------------------------------------------------
+ // GlyphMaker -------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Obtain the GlyphMaker instance for a particular font size.
+ *
+ * @param fontSize the size of these fonts in pixels
+ * @return the instance
+ */
+ public static GlyphMaker getInstance(final int fontSize) {
+ synchronized (GlyphMaker.class) {
+ GlyphMaker maker = makers.get(fontSize);
+ if (maker == null) {
+ maker = new GlyphMaker(fontSize);
+ makers.put(fontSize, maker);
+ }
+ return maker;
+ }
+ }
+
+ /**
+ * Get a glyph image.
+ *
+ * @param cell the character to draw
+ * @param cellWidth the width of the text cell to draw into
+ * @param cellHeight the height of the text cell to draw into
+ * @return the glyph as an image
+ */
+ public BufferedImage getImage(final Cell cell, final int cellWidth,
+ final int cellHeight) {
+
+ return getImage(cell, cellWidth, cellHeight, true);
+ }
+
+ /**
+ * Get a glyph image.
+ *
+ * @param cell the character to draw
+ * @param cellWidth the width of the text cell to draw into
+ * @param cellHeight the height of the text cell to draw into
+ * @param blinkVisible if true, the cell is visible if it is blinking
+ * @return the glyph as an image
+ */
+ public BufferedImage getImage(final Cell cell, final int cellWidth,
+ final int cellHeight, final boolean blinkVisible) {
+
+ int ch = cell.getChar();
+ if (StringUtils.isCjk(ch)) {
+ if (makerCjk.canDisplay(ch)) {
+ return makerCjk.getImage(cell, cellWidth, cellHeight,
+ blinkVisible);
+ }
+ }
+ if (StringUtils.isEmoji(ch)) {
+ if (makerEmoji.canDisplay(ch)) {
+ // System.err.println("emoji: " + String.format("0x%x", ch));
+ return makerEmoji.getImage(cell, cellWidth, cellHeight,
+ blinkVisible);
+ }
+ }
+
+ // When all else fails, use the default.
+ if (makerMono.canDisplay(ch)) {
+ return makerMono.getImage(cell, cellWidth, cellHeight,
+ blinkVisible);
+ }
+
+ return makerFallback.getImage(cell, cellWidth, cellHeight,
+ blinkVisible);
+ }
+
+ }
--- /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.awt.image.BufferedImage;
+
+ import jexer.backend.GlyphMaker;
+ import jexer.bits.Cell;
+ import jexer.bits.CellAttributes;
+ import jexer.bits.GraphicsChars;
+ import jexer.bits.StringUtils;
+
+ /**
+ * A logical screen composed of a 2D array of Cells.
+ */
+ public class LogicalScreen implements Screen {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Width of the visible window.
+ */
+ protected int width;
+
+ /**
+ * Height of the visible window.
+ */
+ protected int height;
+
+ /**
+ * Drawing offset for x.
+ */
+ private int offsetX;
+
+ /**
+ * Drawing offset for y.
+ */
+ private int offsetY;
+
+ /**
+ * Ignore anything drawn right of clipRight.
+ */
+ private int clipRight;
+
+ /**
+ * Ignore anything drawn below clipBottom.
+ */
+ private int clipBottom;
+
+ /**
+ * Ignore anything drawn left of clipLeft.
+ */
+ private int clipLeft;
+
+ /**
+ * Ignore anything drawn above clipTop.
+ */
+ private int clipTop;
+
+ /**
+ * The physical screen last sent out on flush().
+ */
+ protected Cell [][] physical;
+
+ /**
+ * The logical screen being rendered to.
+ */
+ protected Cell [][] logical;
+
+ /**
+ * Set if the user explicitly wants to redraw everything starting with a
+ * ECMATerminal.clearAll().
+ */
+ protected boolean reallyCleared;
+
+ /**
+ * If true, the cursor is visible and should be placed onscreen at
+ * (cursorX, cursorY) during a call to flushPhysical().
+ */
+ protected boolean cursorVisible;
+
+ /**
+ * Cursor X position if visible.
+ */
+ protected int cursorX;
+
+ /**
+ * Cursor Y position if visible.
+ */
+ protected int cursorY;
+
+ /**
+ * The last used height of a character cell in pixels, only used for
+ * full-width chars.
+ */
+ private int lastTextHeight = -1;
+
+ /**
+ * The glyph drawer for full-width chars.
+ */
+ private GlyphMaker glyphMaker = null;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor. Sets everything to not-bold, white-on-black.
+ */
+ protected LogicalScreen() {
+ offsetX = 0;
+ offsetY = 0;
+ width = 80;
+ height = 24;
+ logical = null;
+ physical = null;
+ reallocate(width, height);
+ }
+
+ // ------------------------------------------------------------------------
+ // Screen -----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get the width of a character cell in pixels.
+ *
+ * @return the width in pixels of a character cell
+ */
+ public int getTextWidth() {
+ // Default width is 16 pixels.
+ return 16;
+ }
+
+ /**
+ * Get the height of a character cell in pixels.
+ *
+ * @return the height in pixels of a character cell
+ */
+ public int getTextHeight() {
+ // Default height is 20 pixels.
+ return 20;
+ }
+
+ /**
+ * Set drawing offset for x.
+ *
+ * @param offsetX new drawing offset
+ */
+ public final void setOffsetX(final int offsetX) {
+ this.offsetX = offsetX;
+ }
+
+ /**
+ * Set drawing offset for y.
+ *
+ * @param offsetY new drawing offset
+ */
+ public final void setOffsetY(final int offsetY) {
+ this.offsetY = offsetY;
+ }
+
+ /**
+ * Get right drawing clipping boundary.
+ *
+ * @return drawing boundary
+ */
+ public final int getClipRight() {
+ return clipRight;
+ }
+
+ /**
+ * Set right drawing clipping boundary.
+ *
+ * @param clipRight new boundary
+ */
+ public final void setClipRight(final int clipRight) {
+ this.clipRight = clipRight;
+ }
+
+ /**
+ * Get bottom drawing clipping boundary.
+ *
+ * @return drawing boundary
+ */
+ public final int getClipBottom() {
+ return clipBottom;
+ }
+
+ /**
+ * Set bottom drawing clipping boundary.
+ *
+ * @param clipBottom new boundary
+ */
+ public final void setClipBottom(final int clipBottom) {
+ this.clipBottom = clipBottom;
+ }
+
+ /**
+ * Get left drawing clipping boundary.
+ *
+ * @return drawing boundary
+ */
+ public final int getClipLeft() {
+ return clipLeft;
+ }
+
+ /**
+ * Set left drawing clipping boundary.
+ *
+ * @param clipLeft new boundary
+ */
+ public final void setClipLeft(final int clipLeft) {
+ this.clipLeft = clipLeft;
+ }
+
+ /**
+ * Get top drawing clipping boundary.
+ *
+ * @return drawing boundary
+ */
+ public final int getClipTop() {
+ return clipTop;
+ }
+
+ /**
+ * Set top drawing clipping boundary.
+ *
+ * @param clipTop new boundary
+ */
+ public final void setClipTop(final int clipTop) {
+ this.clipTop = clipTop;
+ }
+
+ /**
+ * Get dirty flag.
+ *
+ * @return if true, the logical screen is not in sync with the physical
+ * screen
+ */
+ public final boolean isDirty() {
+ for (int x = 0; x < width; x++) {
+ for (int y = 0; y < height; y++) {
+ if (!logical[x][y].equals(physical[x][y])) {
+ return true;
+ }
+ if (logical[x][y].isBlink()) {
+ // Blinking screens are always dirty. There is
+ // opportunity for a Netscape blink tag joke here...
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Get the attributes at one location.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @return attributes at (x, y)
+ */
+ public final CellAttributes getAttrXY(final int x, final int y) {
+ CellAttributes attr = new CellAttributes();
+ if ((x >= 0) && (x < width) && (y >= 0) && (y < height)) {
+ attr.setTo(logical[x][y]);
+ }
+ return attr;
+ }
+
+ /**
+ * Get the cell at one location.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @return the character + attributes
+ */
+ public Cell getCharXY(final int x, final int y) {
+ Cell cell = new Cell();
+ if ((x >= 0) && (x < width) && (y >= 0) && (y < height)) {
+ cell.setTo(logical[x][y]);
+ }
+ return cell;
+ }
+
+ /**
+ * Set the attributes at one location.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param attr attributes to use (bold, foreColor, backColor)
+ */
+ public final void putAttrXY(final int x, final int y,
+ final CellAttributes attr) {
+
+ putAttrXY(x, y, attr, true);
+ }
+
+ /**
+ * Set the attributes at one location.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param attr attributes to use (bold, foreColor, backColor)
+ * @param clip if true, honor clipping/offset
+ */
+ public final void putAttrXY(final int x, final int y,
+ final CellAttributes attr, final boolean clip) {
+
+ int X = x;
+ int Y = y;
+
+ if (clip) {
+ if ((x < clipLeft)
+ || (x >= clipRight)
+ || (y < clipTop)
+ || (y >= clipBottom)
+ ) {
+ return;
+ }
+ X += offsetX;
+ Y += offsetY;
+ }
+
+ if ((X >= 0) && (X < width) && (Y >= 0) && (Y < height)) {
+ logical[X][Y].setTo(attr);
+
+ // If this happens to be the cursor position, make the position
+ // dirty.
+ if ((cursorX == X) && (cursorY == Y)) {
+ physical[cursorX][cursorY].unset();
+ unsetImageRow(cursorY);
+ }
+ }
+ }
+
+ /**
+ * Fill the entire screen with one character with attributes.
+ *
+ * @param ch character to draw
+ * @param attr attributes to use (bold, foreColor, backColor)
+ */
+ public final void putAll(final int ch, final CellAttributes attr) {
+
+ for (int x = 0; x < width; x++) {
+ for (int y = 0; y < height; y++) {
+ putCharXY(x, y, ch, attr);
+ }
+ }
+ }
+
+ /**
+ * Render one character with attributes.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param ch character + attributes to draw
+ */
+ public final void putCharXY(final int x, final int y, final Cell ch) {
+ if ((x < clipLeft)
+ || (x >= clipRight)
+ || (y < clipTop)
+ || (y >= clipBottom)
+ ) {
+ return;
+ }
+
+ if ((StringUtils.width(ch.getChar()) == 2) && (!ch.isImage())) {
+ putFullwidthCharXY(x, y, ch);
+ return;
+ }
+
+ int X = x + offsetX;
+ int Y = y + offsetY;
+
+ // System.err.printf("putCharXY: %d, %d, %c\n", X, Y, ch);
+
+ if ((X >= 0) && (X < width) && (Y >= 0) && (Y < height)) {
+
+ // Do not put control characters on the display
+ if (!ch.isImage()) {
+ assert (ch.getChar() >= 0x20);
+ assert (ch.getChar() != 0x7F);
+ }
+ logical[X][Y].setTo(ch);
+
+ // If this happens to be the cursor position, make the position
+ // dirty.
+ if ((cursorX == X) && (cursorY == Y)) {
+ physical[cursorX][cursorY].unset();
+ unsetImageRow(cursorY);
+ }
+ }
+ }
+
+ /**
+ * Render one character with attributes.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param ch character to draw
+ * @param attr attributes to use (bold, foreColor, backColor)
+ */
+ public final void putCharXY(final int x, final int y, final int ch,
+ final CellAttributes attr) {
+
+ if ((x < clipLeft)
+ || (x >= clipRight)
+ || (y < clipTop)
+ || (y >= clipBottom)
+ ) {
+ return;
+ }
+
+ if (StringUtils.width(ch) == 2) {
+ putFullwidthCharXY(x, y, ch, attr);
+ return;
+ }
+
+ int X = x + offsetX;
+ int Y = y + offsetY;
+
+ // System.err.printf("putCharXY: %d, %d, %c\n", X, Y, ch);
+
+ if ((X >= 0) && (X < width) && (Y >= 0) && (Y < height)) {
+
+ // Do not put control characters on the display
+ assert (ch >= 0x20);
+ assert (ch != 0x7F);
+
+ logical[X][Y].setTo(attr);
+ logical[X][Y].setChar(ch);
+
+ // If this happens to be the cursor position, make the position
+ // dirty.
+ if ((cursorX == X) && (cursorY == Y)) {
+ physical[cursorX][cursorY].unset();
+ unsetImageRow(cursorY);
+ }
+ }
+ }
+
+ /**
+ * Render one character without changing the underlying attributes.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param ch character to draw
+ */
+ public final void putCharXY(final int x, final int y, final int ch) {
+ if ((x < clipLeft)
+ || (x >= clipRight)
+ || (y < clipTop)
+ || (y >= clipBottom)
+ ) {
+ return;
+ }
+
+ if (StringUtils.width(ch) == 2) {
+ putFullwidthCharXY(x, y, ch);
+ return;
+ }
+
+ int X = x + offsetX;
+ int Y = y + offsetY;
+
+ // System.err.printf("putCharXY: %d, %d, %c\n", X, Y, ch);
+
+ if ((X >= 0) && (X < width) && (Y >= 0) && (Y < height)) {
+ logical[X][Y].setChar(ch);
+
+ // If this happens to be the cursor position, make the position
+ // dirty.
+ if ((cursorX == X) && (cursorY == Y)) {
+ physical[cursorX][cursorY].unset();
+ unsetImageRow(cursorY);
+ }
+ }
+ }
+
+ /**
+ * Render a string. Does not wrap if the string exceeds the line.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param str string to draw
+ * @param attr attributes to use (bold, foreColor, backColor)
+ */
+ public final void putStringXY(final int x, final int y, final String str,
+ final CellAttributes attr) {
+
+ int i = x;
+ for (int j = 0; j < str.length();) {
+ int ch = str.codePointAt(j);
+ j += Character.charCount(ch);
+ putCharXY(i, y, ch, attr);
+ i += StringUtils.width(ch);
+ if (i == width) {
+ break;
+ }
+ }
+ }
+
+ /**
+ * Render a string without changing the underlying attribute. Does not
+ * wrap if the string exceeds the line.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param str string to draw
+ */
+ public final void putStringXY(final int x, final int y, final String str) {
+
+ int i = x;
+ for (int j = 0; j < str.length();) {
+ int ch = str.codePointAt(j);
+ j += Character.charCount(ch);
+ putCharXY(i, y, ch);
+ i += StringUtils.width(ch);
+ if (i == width) {
+ break;
+ }
+ }
+ }
+
+ /**
+ * Draw a vertical line from (x, y) to (x, y + n).
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param n number of characters to draw
+ * @param ch character to draw
+ * @param attr attributes to use (bold, foreColor, backColor)
+ */
+ public final void vLineXY(final int x, final int y, final int n,
+ final int ch, final CellAttributes attr) {
+
+ for (int i = y; i < y + n; i++) {
+ putCharXY(x, i, ch, attr);
+ }
+ }
+
+ /**
+ * Draw a horizontal line from (x, y) to (x + n, y).
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param n number of characters to draw
+ * @param ch character to draw
+ * @param attr attributes to use (bold, foreColor, backColor)
+ */
+ public final void hLineXY(final int x, final int y, final int n,
+ final int ch, final CellAttributes attr) {
+
+ for (int i = x; i < x + n; i++) {
+ putCharXY(i, y, ch, attr);
+ }
+ }
+
+ /**
+ * Change the width. Everything on-screen will be destroyed and must be
+ * redrawn.
+ *
+ * @param width new screen width
+ */
+ public final synchronized void setWidth(final int width) {
+ reallocate(width, this.height);
+ }
+
+ /**
+ * Change the height. Everything on-screen will be destroyed and must be
+ * redrawn.
+ *
+ * @param height new screen height
+ */
+ public final synchronized void setHeight(final int height) {
+ reallocate(this.width, height);
+ }
+
+ /**
+ * Change the width and height. Everything on-screen will be destroyed
+ * and must be redrawn.
+ *
+ * @param width new screen width
+ * @param height new screen height
+ */
+ public final void setDimensions(final int width, final int height) {
+ reallocate(width, height);
+ resizeToScreen();
+ }
+
+ /**
+ * Resize the physical screen to match the logical screen dimensions.
+ */
+ public void resizeToScreen() {
+ // Subclasses are expected to override this.
+ }
+
+ /**
+ * Get the height.
+ *
+ * @return current screen height
+ */
+ public final synchronized int getHeight() {
+ return this.height;
+ }
+
+ /**
+ * Get the width.
+ *
+ * @return current screen width
+ */
+ public final synchronized int getWidth() {
+ return this.width;
+ }
+
+ /**
+ * Reset screen to not-bold, white-on-black. Also flushes the offset and
+ * clip variables.
+ */
+ public final synchronized void reset() {
+ for (int row = 0; row < height; row++) {
+ for (int col = 0; col < width; col++) {
+ logical[col][row].reset();
+ }
+ }
+ resetClipping();
+ }
+
+ /**
+ * Flush the offset and clip variables.
+ */
+ public final void resetClipping() {
+ offsetX = 0;
+ offsetY = 0;
+ clipLeft = 0;
+ clipTop = 0;
+ clipRight = width;
+ clipBottom = height;
+ }
+
+ /**
+ * Clear the logical screen.
+ */
+ public final void clear() {
+ reset();
+ }
+
+ /**
+ * Draw a box with a border and empty background.
+ *
+ * @param left left column of box. 0 is the left-most column.
+ * @param top top row of the box. 0 is the top-most row.
+ * @param right right column of box
+ * @param bottom bottom row of the box
+ * @param border attributes to use for the border
+ * @param background attributes to use for the background
+ */
+ public final void drawBox(final int left, final int top,
+ final int right, final int bottom,
+ final CellAttributes border, final CellAttributes background) {
+
+ drawBox(left, top, right, bottom, border, background, 1, false);
+ }
+
+ /**
+ * Draw a box with a border and empty background.
+ *
+ * @param left left column of box. 0 is the left-most column.
+ * @param top top row of the box. 0 is the top-most row.
+ * @param right right column of box
+ * @param bottom bottom row of the box
+ * @param border attributes to use for the border
+ * @param background attributes to use for the background
+ * @param borderType if 1, draw a single-line border; if 2, draw a
+ * double-line border; if 3, draw double-line top/bottom edges and
+ * single-line left/right edges (like Qmodem)
+ * @param shadow if true, draw a "shadow" on the box
+ */
+ public final void drawBox(final int left, final int top,
+ final int right, final int bottom,
+ final CellAttributes border, final CellAttributes background,
+ final int borderType, final boolean shadow) {
+
+ int boxWidth = right - left;
+ int boxHeight = bottom - top;
+
+ char cTopLeft;
+ char cTopRight;
+ char cBottomLeft;
+ char cBottomRight;
+ char cHSide;
+ char cVSide;
+
+ switch (borderType) {
+ case 1:
+ cTopLeft = GraphicsChars.ULCORNER;
+ cTopRight = GraphicsChars.URCORNER;
+ cBottomLeft = GraphicsChars.LLCORNER;
+ cBottomRight = GraphicsChars.LRCORNER;
+ cHSide = GraphicsChars.SINGLE_BAR;
+ cVSide = GraphicsChars.WINDOW_SIDE;
+ break;
+
+ case 2:
+ cTopLeft = GraphicsChars.WINDOW_LEFT_TOP_DOUBLE;
+ cTopRight = GraphicsChars.WINDOW_RIGHT_TOP_DOUBLE;
+ cBottomLeft = GraphicsChars.WINDOW_LEFT_BOTTOM_DOUBLE;
+ cBottomRight = GraphicsChars.WINDOW_RIGHT_BOTTOM_DOUBLE;
+ cHSide = GraphicsChars.DOUBLE_BAR;
+ cVSide = GraphicsChars.WINDOW_SIDE_DOUBLE;
+ break;
+
+ case 3:
+ cTopLeft = GraphicsChars.WINDOW_LEFT_TOP;
+ cTopRight = GraphicsChars.WINDOW_RIGHT_TOP;
+ cBottomLeft = GraphicsChars.WINDOW_LEFT_BOTTOM;
+ cBottomRight = GraphicsChars.WINDOW_RIGHT_BOTTOM;
+ cHSide = GraphicsChars.WINDOW_TOP;
+ cVSide = GraphicsChars.WINDOW_SIDE;
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid border type: "
+ + borderType);
+ }
+
+ // Place the corner characters
+ putCharXY(left, top, cTopLeft, border);
+ putCharXY(left + boxWidth - 1, top, cTopRight, border);
+ putCharXY(left, top + boxHeight - 1, cBottomLeft, border);
+ putCharXY(left + boxWidth - 1, top + boxHeight - 1, cBottomRight,
+ border);
+
+ // Draw the box lines
+ hLineXY(left + 1, top, boxWidth - 2, cHSide, border);
+ vLineXY(left, top + 1, boxHeight - 2, cVSide, border);
+ hLineXY(left + 1, top + boxHeight - 1, boxWidth - 2, cHSide, border);
+ vLineXY(left + boxWidth - 1, top + 1, boxHeight - 2, cVSide, border);
+
+ // Fill in the interior background
+ for (int i = 1; i < boxHeight - 1; i++) {
+ hLineXY(1 + left, i + top, boxWidth - 2, ' ', background);
+ }
+
+ if (shadow) {
+ // Draw a shadow
+ drawBoxShadow(left, top, right, bottom);
+ }
+ }
+
+ /**
+ * Draw a box shadow.
+ *
+ * @param left left column of box. 0 is the left-most column.
+ * @param top top row of the box. 0 is the top-most row.
+ * @param right right column of box
+ * @param bottom bottom row of the box
+ */
+ public final void drawBoxShadow(final int left, final int top,
+ final int right, final int bottom) {
+
+ int boxTop = top;
+ int boxLeft = left;
+ int boxWidth = right - left;
+ int boxHeight = bottom - top;
+ CellAttributes shadowAttr = new CellAttributes();
+
+ // Shadows do not honor clipping but they DO honor offset.
+ int oldClipRight = clipRight;
+ int oldClipBottom = clipBottom;
+ // When offsetX or offsetY go negative, we need to increase the clip
+ // bounds.
+ clipRight = width - offsetX;
+ clipBottom = height - offsetY;
+
+ for (int i = 0; i < boxHeight; i++) {
+ Cell cell = getCharXY(offsetX + boxLeft + boxWidth,
+ offsetY + boxTop + 1 + i);
+ if (cell.getWidth() == Cell.Width.SINGLE) {
+ putAttrXY(boxLeft + boxWidth, boxTop + 1 + i, shadowAttr);
+ } else {
+ putCharXY(boxLeft + boxWidth, boxTop + 1 + i, ' ', shadowAttr);
+ }
+ cell = getCharXY(offsetX + boxLeft + boxWidth + 1,
+ offsetY + boxTop + 1 + i);
+ if (cell.getWidth() == Cell.Width.SINGLE) {
+ putAttrXY(boxLeft + boxWidth + 1, boxTop + 1 + i, shadowAttr);
+ } else {
+ putCharXY(boxLeft + boxWidth + 1, boxTop + 1 + i, ' ',
+ shadowAttr);
+ }
+ }
+ for (int i = 0; i < boxWidth; i++) {
+ Cell cell = getCharXY(offsetX + boxLeft + 2 + i,
+ offsetY + boxTop + boxHeight);
+ if (cell.getWidth() == Cell.Width.SINGLE) {
+ putAttrXY(boxLeft + 2 + i, boxTop + boxHeight, shadowAttr);
+ } else {
+ putCharXY(boxLeft + 2 + i, boxTop + boxHeight, ' ', shadowAttr);
+ }
+ }
+ clipRight = oldClipRight;
+ clipBottom = oldClipBottom;
+ }
+
+ /**
+ * Default implementation does nothing.
+ */
+ public void flushPhysical() {}
+
+ /**
+ * Put the cursor at (x,y).
+ *
+ * @param visible if true, the cursor should be visible
+ * @param x column coordinate to put the cursor on
+ * @param y row coordinate to put the cursor on
+ */
+ public void putCursor(final boolean visible, final int x, final int y) {
+ if ((cursorY >= 0)
+ && (cursorX >= 0)
+ && (cursorY <= height - 1)
+ && (cursorX <= width - 1)
+ ) {
+ // Make the current cursor position dirty
+ physical[cursorX][cursorY].unset();
+ unsetImageRow(cursorY);
+ }
+
+ cursorVisible = visible;
+ cursorX = x;
+ cursorY = y;
+ }
+
+ /**
+ * Hide the cursor.
+ */
+ public final void hideCursor() {
+ cursorVisible = false;
+ }
+
+ /**
+ * Get the cursor visibility.
+ *
+ * @return true if the cursor is visible
+ */
+ public boolean isCursorVisible() {
+ return cursorVisible;
+ }
+
+ /**
+ * Get the cursor X position.
+ *
+ * @return the cursor x column position
+ */
+ public int getCursorX() {
+ return cursorX;
+ }
+
+ /**
+ * Get the cursor Y position.
+ *
+ * @return the cursor y row position
+ */
+ public int getCursorY() {
+ return cursorY;
+ }
+
+ /**
+ * Set the window title. Default implementation does nothing.
+ *
+ * @param title the new title
+ */
+ public void setTitle(final String title) {}
+
+ // ------------------------------------------------------------------------
+ // LogicalScreen ----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Reallocate screen buffers.
+ *
+ * @param width new width
+ * @param height new height
+ */
+ private synchronized void reallocate(final int width, final int height) {
+ if (logical != null) {
+ for (int row = 0; row < this.height; row++) {
+ for (int col = 0; col < this.width; col++) {
+ logical[col][row] = null;
+ }
+ }
+ logical = null;
+ }
+ logical = new Cell[width][height];
+ if (physical != null) {
+ for (int row = 0; row < this.height; row++) {
+ for (int col = 0; col < this.width; col++) {
+ physical[col][row] = null;
+ }
+ }
+ physical = null;
+ }
+ physical = new Cell[width][height];
+
+ for (int row = 0; row < height; row++) {
+ for (int col = 0; col < width; col++) {
+ physical[col][row] = new Cell();
+ logical[col][row] = new Cell();
+ }
+ }
+
+ this.width = width;
+ this.height = height;
+
+ clipLeft = 0;
+ clipTop = 0;
+ clipRight = width;
+ clipBottom = height;
+
+ reallyCleared = true;
+ }
+
+ /**
+ * Clear the physical screen.
+ */
+ public final void clearPhysical() {
+ for (int row = 0; row < height; row++) {
+ for (int col = 0; col < width; col++) {
+ physical[col][row].unset();
+ }
+ }
+ }
+
+ /**
+ * Unset every image cell on one row of the physical screen, forcing
+ * images on that row to be redrawn.
+ *
+ * @param y row coordinate. 0 is the top-most row.
+ */
+ public final void unsetImageRow(final int y) {
+ if ((y < 0) || (y >= height)) {
+ return;
+ }
+ for (int x = 0; x < width; x++) {
+ if (logical[x][y].isImage()) {
+ physical[x][y].unset();
+ }
+ }
+ }
+
+ /**
+ * Render one fullwidth cell.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param cell the cell to draw
+ */
+ public final void putFullwidthCharXY(final int x, final int y,
+ final Cell cell) {
+
+ int cellWidth = getTextWidth();
+ int cellHeight = getTextHeight();
+
+ if (lastTextHeight != cellHeight) {
+ glyphMaker = GlyphMaker.getInstance(cellHeight);
+ lastTextHeight = cellHeight;
+ }
+ BufferedImage image = glyphMaker.getImage(cell, cellWidth * 2,
+ cellHeight);
+ BufferedImage leftImage = image.getSubimage(0, 0, cellWidth,
+ cellHeight);
+ BufferedImage rightImage = image.getSubimage(cellWidth, 0, cellWidth,
+ cellHeight);
+
+ Cell left = new Cell(cell);
+ left.setImage(leftImage);
+ left.setWidth(Cell.Width.LEFT);
+ putCharXY(x, y, left);
+
+ Cell right = new Cell(cell);
+ right.setImage(rightImage);
+ right.setWidth(Cell.Width.RIGHT);
+ putCharXY(x + 1, y, right);
+ }
+
+ /**
+ * Render one fullwidth character with attributes.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param ch character to draw
+ * @param attr attributes to use (bold, foreColor, backColor)
+ */
+ public final void putFullwidthCharXY(final int x, final int y,
+ final int ch, final CellAttributes attr) {
+
+ Cell cell = new Cell(ch, attr);
+ putFullwidthCharXY(x, y, cell);
+ }
+
+ /**
+ * Render one fullwidth character with attributes.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param ch character to draw
+ */
+ public final void putFullwidthCharXY(final int x, final int y,
+ final int ch) {
+
+ Cell cell = new Cell(ch);
+ cell.setAttr(getAttrXY(x, y));
+ putFullwidthCharXY(x, y, cell);
+ }
+
+ }
--- /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.ArrayList;
+ import java.util.List;
+
+ import jexer.event.TCommandEvent;
+ import jexer.event.TInputEvent;
+ import static jexer.TCommand.*;
+
+ /**
+ * MultiBackend mirrors its I/O to several backends.
+ */
+ public class MultiBackend implements Backend {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The screen to use.
+ */
+ private MultiScreen multiScreen;
+
+ /**
+ * The list of backends to use.
+ */
+ private List<Backend> backends = new ArrayList<Backend>();
+
+ /**
+ * The SessionInfo to return.
+ */
+ private SessionInfo sessionInfo;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor requires one backend. Note that this backend's
+ * screen will be replaced with a MultiScreen.
+ *
+ * @param backend the backend to add
+ */
+ public MultiBackend(final Backend backend) {
+ backends.add(backend);
+ if (backend instanceof TWindowBackend) {
+ multiScreen = new MultiScreen(((TWindowBackend) backend).getOtherScreen());
+ } else {
+ multiScreen = new MultiScreen(backend.getScreen());
+ }
+ if (backend instanceof GenericBackend) {
+ ((GenericBackend) backend).abortOnDisconnect = false;
+ }
+ sessionInfo = backend.getSessionInfo();
+ }
+
+ // ------------------------------------------------------------------------
+ // Backend ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Getter for sessionInfo.
+ *
+ * @return the SessionInfo
+ */
+ public SessionInfo getSessionInfo() {
+ return sessionInfo;
+ }
+
+ /**
+ * Getter for screen.
+ *
+ * @return the Screen
+ */
+ public Screen getScreen() {
+ return multiScreen;
+ }
+
+ /**
+ * Subclasses must provide an implementation that syncs the logical
+ * screen to the physical device.
+ */
+ public void flushScreen() {
+ for (Backend backend: backends) {
+ backend.flushScreen();
+ }
+ }
+
+ /**
+ * Check if there are events in the queue.
+ *
+ * @return if true, getEvents() has something to return to the application
+ */
+ public boolean hasEvents() {
+ if (backends.size() == 0) {
+ return true;
+ }
+ for (Backend backend: backends) {
+ if (backend.hasEvents()) {
+ return true;
+ }
+ }
+ 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) {
+ List<Backend> backendsToRemove = null;
+ for (Backend backend: backends) {
+ if (backend.hasEvents()) {
+ backend.getEvents(queue);
+
+ // This default backend assumes a single user, and if that
+ // user becomes disconnected we should terminate the
+ // application.
+ if (queue.size() > 0) {
+ TInputEvent event = queue.get(queue.size() - 1);
+ if (event instanceof TCommandEvent) {
+ TCommandEvent command = (TCommandEvent) event;
+ if (command.equals(cmBackendDisconnect)) {
+ if (backendsToRemove == null) {
+ backendsToRemove = new ArrayList<Backend>();
+ }
+ backendsToRemove.add(backend);
+ }
+ }
+ }
+ }
+ }
+ if (backendsToRemove != null) {
+ for (Backend backend: backendsToRemove) {
+ multiScreen.removeScreen(backend.getScreen());
+ backends.remove(backend);
+ backend.shutdown();
+ }
+ }
+ if (backends.size() == 0) {
+ queue.add(new TCommandEvent(cmAbort));
+ }
+ }
+
+ /**
+ * Subclasses must provide an implementation that closes sockets,
+ * restores console, etc.
+ */
+ public void shutdown() {
+ for (Backend backend: backends) {
+ backend.shutdown();
+ }
+ }
+
+ /**
+ * Subclasses must provide an implementation that sets the window title.
+ *
+ * @param title the new title
+ */
+ public void setTitle(final String title) {
+ for (Backend backend: backends) {
+ backend.setTitle(title);
+ }
+ }
+
+ /**
+ * 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) {
+ for (Backend backend: backends) {
+ backend.setListener(listener);
+ }
+ }
+
+ /**
+ * Reload backend options from System properties.
+ */
+ public void reloadOptions() {
+ for (Backend backend: backends) {
+ backend.reloadOptions();
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // MultiBackend -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Add a backend to the list.
+ *
+ * @param backend the backend to add
+ */
+ public void addBackend(final Backend backend) {
+ backends.add(backend);
+ if (backend instanceof TWindowBackend) {
+ multiScreen.addScreen(((TWindowBackend) backend).getOtherScreen());
+ } else {
+ multiScreen.addScreen(backend.getScreen());
+ }
+ if (backend instanceof GenericBackend) {
+ ((GenericBackend) backend).abortOnDisconnect = false;
+ }
+ }
+
+ /**
+ * Remove a backend from the list.
+ *
+ * @param backend the backend to remove
+ */
+ public void removeBackend(final Backend backend) {
+ if (backends.size() > 1) {
+ if (backend instanceof TWindowBackend) {
+ multiScreen.removeScreen(((TWindowBackend) backend).getOtherScreen());
+ } else {
+ multiScreen.removeScreen(backend.getScreen());
+ }
+ backends.remove(backend);
+ }
+ }
+
+ }
--- /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.ArrayList;
+ import java.util.List;
+
+ import jexer.bits.Cell;
+ import jexer.bits.CellAttributes;
+
+ /**
+ * MultiScreen mirrors its I/O to several screens.
+ */
+ public class MultiScreen implements Screen {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The list of screens to use.
+ */
+ private List<Screen> screens = new ArrayList<Screen>();
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor requires one screen.
+ *
+ * @param screen the screen to add
+ */
+ public MultiScreen(final Screen screen) {
+ screens.add(screen);
+ }
+
+ // ------------------------------------------------------------------------
+ // Screen -----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Set drawing offset for x.
+ *
+ * @param offsetX new drawing offset
+ */
+ public void setOffsetX(final int offsetX) {
+ for (Screen screen: screens) {
+ screen.setOffsetX(offsetX);
+ }
+ }
+
+ /**
+ * Set drawing offset for y.
+ *
+ * @param offsetY new drawing offset
+ */
+ public void setOffsetY(final int offsetY) {
+ for (Screen screen: screens) {
+ screen.setOffsetY(offsetY);
+ }
+ }
+
+ /**
+ * Get right drawing clipping boundary.
+ *
+ * @return drawing boundary
+ */
+ public int getClipRight() {
+ return screens.get(0).getClipRight();
+ }
+
+ /**
+ * Set right drawing clipping boundary.
+ *
+ * @param clipRight new boundary
+ */
+ public void setClipRight(final int clipRight) {
+ for (Screen screen: screens) {
+ screen.setClipRight(clipRight);
+ }
+ }
+
+ /**
+ * Get bottom drawing clipping boundary.
+ *
+ * @return drawing boundary
+ */
+ public int getClipBottom() {
+ return screens.get(0).getClipBottom();
+ }
+
+ /**
+ * Set bottom drawing clipping boundary.
+ *
+ * @param clipBottom new boundary
+ */
+ public void setClipBottom(final int clipBottom) {
+ for (Screen screen: screens) {
+ screen.setClipBottom(clipBottom);
+ }
+ }
+
+ /**
+ * Get left drawing clipping boundary.
+ *
+ * @return drawing boundary
+ */
+ public int getClipLeft() {
+ return screens.get(0).getClipLeft();
+ }
+
+ /**
+ * Set left drawing clipping boundary.
+ *
+ * @param clipLeft new boundary
+ */
+ public void setClipLeft(final int clipLeft) {
+ for (Screen screen: screens) {
+ screen.setClipLeft(clipLeft);
+ }
+ }
+
+ /**
+ * Get top drawing clipping boundary.
+ *
+ * @return drawing boundary
+ */
+ public int getClipTop() {
+ return screens.get(0).getClipTop();
+ }
+
+ /**
+ * Set top drawing clipping boundary.
+ *
+ * @param clipTop new boundary
+ */
+ public void setClipTop(final int clipTop) {
+ for (Screen screen: screens) {
+ screen.setClipTop(clipTop);
+ }
+ }
+
+ /**
+ * Get dirty flag.
+ *
+ * @return if true, the logical screen is not in sync with the physical
+ * screen
+ */
+ public boolean isDirty() {
+ for (Screen screen: screens) {
+ if (screen.isDirty()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Get the attributes at one location.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @return attributes at (x, y)
+ */
+ public CellAttributes getAttrXY(final int x, final int y) {
+ return screens.get(0).getAttrXY(x, y);
+ }
+
+ /**
+ * Get the cell at one location.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @return the character + attributes
+ */
+ public Cell getCharXY(final int x, final int y) {
+ return screens.get(0).getCharXY(x, y);
+ }
+
+ /**
+ * Set the attributes at one location.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param attr attributes to use (bold, foreColor, backColor)
+ */
+ public void putAttrXY(final int x, final int y,
+ final CellAttributes attr) {
+
+ for (Screen screen: screens) {
+ screen.putAttrXY(x, y, attr);
+ }
+ }
+
+ /**
+ * Set the attributes at one location.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param attr attributes to use (bold, foreColor, backColor)
+ * @param clip if true, honor clipping/offset
+ */
+ public void putAttrXY(final int x, final int y,
+ final CellAttributes attr, final boolean clip) {
+
+ for (Screen screen: screens) {
+ screen.putAttrXY(x, y, attr, clip);
+ }
+ }
+
+ /**
+ * Fill the entire screen with one character with attributes.
+ *
+ * @param ch character to draw
+ * @param attr attributes to use (bold, foreColor, backColor)
+ */
+ public void putAll(final int ch, final CellAttributes attr) {
+ for (Screen screen: screens) {
+ screen.putAll(ch, attr);
+ }
+ }
+
+ /**
+ * Render one character with attributes.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param ch character + attributes to draw
+ */
+ public void putCharXY(final int x, final int y, final Cell ch) {
+ for (Screen screen: screens) {
+ screen.putCharXY(x, y, ch);
+ }
+ }
+
+ /**
+ * Render one character with attributes.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param ch character to draw
+ * @param attr attributes to use (bold, foreColor, backColor)
+ */
+ public void putCharXY(final int x, final int y, final int ch,
+ final CellAttributes attr) {
+
+ for (Screen screen: screens) {
+ screen.putCharXY(x, y, ch, attr);
+ }
+ }
+
+ /**
+ * Render one character without changing the underlying attributes.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param ch character to draw
+ */
+ public void putCharXY(final int x, final int y, final int ch) {
+ for (Screen screen: screens) {
+ screen.putCharXY(x, y, ch);
+ }
+ }
+
+ /**
+ * Render a string. Does not wrap if the string exceeds the line.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param str string to draw
+ * @param attr attributes to use (bold, foreColor, backColor)
+ */
+ public void putStringXY(final int x, final int y, final String str,
+ final CellAttributes attr) {
+
+ for (Screen screen: screens) {
+ screen.putStringXY(x, y, str, attr);
+ }
+ }
+
+ /**
+ * Render a string without changing the underlying attribute. Does not
+ * wrap if the string exceeds the line.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param str string to draw
+ */
+ public void putStringXY(final int x, final int y, final String str) {
+ for (Screen screen: screens) {
+ screen.putStringXY(x, y, str);
+ }
+ }
+
+ /**
+ * Draw a vertical line from (x, y) to (x, y + n).
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param n number of characters to draw
+ * @param ch character to draw
+ * @param attr attributes to use (bold, foreColor, backColor)
+ */
+ public void vLineXY(final int x, final int y, final int n,
+ final int ch, final CellAttributes attr) {
+
+ for (Screen screen: screens) {
+ screen.vLineXY(x, y, n, ch, attr);
+ }
+ }
+
+ /**
+ * Draw a horizontal line from (x, y) to (x + n, y).
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param n number of characters to draw
+ * @param ch character to draw
+ * @param attr attributes to use (bold, foreColor, backColor)
+ */
+ public void hLineXY(final int x, final int y, final int n,
+ final int ch, final CellAttributes attr) {
+
+ for (Screen screen: screens) {
+ screen.hLineXY(x, y, n, ch, attr);
+ }
+ }
+
+ /**
+ * Change the width. Everything on-screen will be destroyed and must be
+ * redrawn.
+ *
+ * @param width new screen width
+ */
+ public void setWidth(final int width) {
+ for (Screen screen: screens) {
+ screen.setWidth(width);
+ }
+ }
+
+ /**
+ * Change the height. Everything on-screen will be destroyed and must be
+ * redrawn.
+ *
+ * @param height new screen height
+ */
+ public void setHeight(final int height) {
+ for (Screen screen: screens) {
+ screen.setHeight(height);
+ }
+ }
+
+ /**
+ * Change the width and height. Everything on-screen will be destroyed
+ * and must be redrawn.
+ *
+ * @param width new screen width
+ * @param height new screen height
+ */
+ public void setDimensions(final int width, final int height) {
+ for (Screen screen: screens) {
+ // Do not blindly call setDimension() on every screen. Instead
+ // call it only on those screens that do not already have the
+ // requested dimension. With this very small check, we have the
+ // ability for ANY screen in the MultiBackend to resize ALL of
+ // the screens.
+ if ((screen.getWidth() != width)
+ || (screen.getHeight() != height)
+ ) {
+ screen.setDimensions(width, height);
+ } else {
+ // The screen that didn't change is probably the one that
+ // prompted the resize. Force it to repaint.
+ screen.clearPhysical();
+ }
+ }
+ }
+
+ /**
+ * Get the height.
+ *
+ * @return current screen height
+ */
+ public int getHeight() {
+ // Return the smallest height of the screens.
+ int height = screens.get(0).getHeight();
+ for (Screen screen: screens) {
+ if (screen.getHeight() < height) {
+ height = screen.getHeight();
+ }
+ }
+ return height;
+ }
+
+ /**
+ * Get the width.
+ *
+ * @return current screen width
+ */
+ public int getWidth() {
+ // Return the smallest width of the screens.
+ int width = screens.get(0).getWidth();
+ for (Screen screen: screens) {
+ if (screen.getWidth() < width) {
+ width = screen.getWidth();
+ }
+ }
+ return width;
+ }
+
+ /**
+ * Reset screen to not-bold, white-on-black. Also flushes the offset and
+ * clip variables.
+ */
+ public void reset() {
+ for (Screen screen: screens) {
+ screen.reset();
+ }
+ }
+
+ /**
+ * Flush the offset and clip variables.
+ */
+ public void resetClipping() {
+ for (Screen screen: screens) {
+ screen.resetClipping();
+ }
+ }
+
+ /**
+ * Clear the logical screen.
+ */
+ public void clear() {
+ for (Screen screen: screens) {
+ screen.clear();
+ }
+ }
+
+ /**
+ * Draw a box with a border and empty background.
+ *
+ * @param left left column of box. 0 is the left-most row.
+ * @param top top row of the box. 0 is the top-most row.
+ * @param right right column of box
+ * @param bottom bottom row of the box
+ * @param border attributes to use for the border
+ * @param background attributes to use for the background
+ */
+ public void drawBox(final int left, final int top,
+ final int right, final int bottom,
+ final CellAttributes border, final CellAttributes background) {
+
+ for (Screen screen: screens) {
+ screen.drawBox(left, top, right, bottom, border, background);
+ }
+ }
+
+ /**
+ * Draw a box with a border and empty background.
+ *
+ * @param left left column of box. 0 is the left-most row.
+ * @param top top row of the box. 0 is the top-most row.
+ * @param right right column of box
+ * @param bottom bottom row of the box
+ * @param border attributes to use for the border
+ * @param background attributes to use for the background
+ * @param borderType if 1, draw a single-line border; if 2, draw a
+ * double-line border; if 3, draw double-line top/bottom edges and
+ * single-line left/right edges (like Qmodem)
+ * @param shadow if true, draw a "shadow" on the box
+ */
+ public void drawBox(final int left, final int top,
+ final int right, final int bottom,
+ final CellAttributes border, final CellAttributes background,
+ final int borderType, final boolean shadow) {
+
+ for (Screen screen: screens) {
+ screen.drawBox(left, top, right, bottom, border, background,
+ borderType, shadow);
+ }
+ }
+
+ /**
+ * Draw a box shadow.
+ *
+ * @param left left column of box. 0 is the left-most row.
+ * @param top top row of the box. 0 is the top-most row.
+ * @param right right column of box
+ * @param bottom bottom row of the box
+ */
+ public void drawBoxShadow(final int left, final int top,
+ final int right, final int bottom) {
+
+ for (Screen screen: screens) {
+ screen.drawBoxShadow(left, top, right, bottom);
+ }
+ }
+
+ /**
+ * Clear the physical screen.
+ */
+ public void clearPhysical() {
+ for (Screen screen: screens) {
+ screen.clearPhysical();
+ }
+ }
+
+ /**
+ * Unset every image cell on one row of the physical screen, forcing
+ * images on that row to be redrawn.
+ *
+ * @param y row coordinate. 0 is the top-most row.
+ */
+ public final void unsetImageRow(final int y) {
+ for (Screen screen: screens) {
+ screen.unsetImageRow(y);
+ }
+ }
+
+ /**
+ * Classes must provide an implementation to push the logical screen to
+ * the physical device.
+ */
+ public void flushPhysical() {
+ for (Screen screen: screens) {
+ screen.flushPhysical();
+ }
+ }
+
+ /**
+ * Put the cursor at (x,y).
+ *
+ * @param visible if true, the cursor should be visible
+ * @param x column coordinate to put the cursor on
+ * @param y row coordinate to put the cursor on
+ */
+ public void putCursor(final boolean visible, final int x, final int y) {
+ for (Screen screen: screens) {
+ screen.putCursor(visible, x, y);
+ }
+ }
+
+ /**
+ * Hide the cursor.
+ */
+ public void hideCursor() {
+ for (Screen screen: screens) {
+ screen.hideCursor();
+ }
+ }
+
+ /**
+ * Get the cursor visibility.
+ *
+ * @return true if the cursor is visible
+ */
+ public boolean isCursorVisible() {
+ return screens.get(0).isCursorVisible();
+ }
+
+ /**
+ * Get the cursor X position.
+ *
+ * @return the cursor x column position
+ */
+ public int getCursorX() {
+ return screens.get(0).getCursorX();
+ }
+
+ /**
+ * Get the cursor Y position.
+ *
+ * @return the cursor y row position
+ */
+ public int getCursorY() {
+ return screens.get(0).getCursorY();
+ }
+
+ /**
+ * Set the window title.
+ *
+ * @param title the new title
+ */
+ public void setTitle(final String title) {
+ for (Screen screen: screens) {
+ screen.setTitle(title);
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // MultiScreen ------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Add a screen to the list.
+ *
+ * @param screen the screen to add
+ */
+ public void addScreen(final Screen screen) {
+ screens.add(screen);
+ }
+
+ /**
+ * Remove a screen from the list.
+ *
+ * @param screen the screen to remove
+ */
+ public void removeScreen(final Screen screen) {
+ if (screens.size() > 1) {
+ screens.remove(screen);
+ }
+ }
+
+ /**
+ * Get the width of a character cell in pixels.
+ *
+ * @return the width in pixels of a character cell
+ */
+ public int getTextWidth() {
+ int textWidth = 16;
+ for (Screen screen: screens) {
+ int newTextWidth = screen.getTextWidth();
+ if (newTextWidth < textWidth) {
+ textWidth = newTextWidth;
+ }
+ }
+ return textWidth;
+ }
+
+ /**
+ * Get the height of a character cell in pixels.
+ *
+ * @return the height in pixels of a character cell
+ */
+ public int getTextHeight() {
+ int textHeight = 20;
+ for (Screen screen: screens) {
+ int newTextHeight = screen.getTextHeight();
+ if (newTextHeight < textHeight) {
+ textHeight = newTextHeight;
+ }
+ }
+ return textHeight;
+ }
+
+ }
--- /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 jexer.bits.Cell;
+ import jexer.bits.CellAttributes;
+
+ /**
+ * Drawing operations API.
+ */
+ public interface Screen {
+
+ /**
+ * Set drawing offset for x.
+ *
+ * @param offsetX new drawing offset
+ */
+ public void setOffsetX(final int offsetX);
+
+ /**
+ * Set drawing offset for y.
+ *
+ * @param offsetY new drawing offset
+ */
+ public void setOffsetY(final int offsetY);
+
+ /**
+ * Get right drawing clipping boundary.
+ *
+ * @return drawing boundary
+ */
+ public int getClipRight();
+
+ /**
+ * Set right drawing clipping boundary.
+ *
+ * @param clipRight new boundary
+ */
+ public void setClipRight(final int clipRight);
+
+ /**
+ * Get bottom drawing clipping boundary.
+ *
+ * @return drawing boundary
+ */
+ public int getClipBottom();
+
+ /**
+ * Set bottom drawing clipping boundary.
+ *
+ * @param clipBottom new boundary
+ */
+ public void setClipBottom(final int clipBottom);
+
+ /**
+ * Get left drawing clipping boundary.
+ *
+ * @return drawing boundary
+ */
+ public int getClipLeft();
+
+ /**
+ * Set left drawing clipping boundary.
+ *
+ * @param clipLeft new boundary
+ */
+ public void setClipLeft(final int clipLeft);
+
+ /**
+ * Get top drawing clipping boundary.
+ *
+ * @return drawing boundary
+ */
+ public int getClipTop();
+
+ /**
+ * Set top drawing clipping boundary.
+ *
+ * @param clipTop new boundary
+ */
+ public void setClipTop(final int clipTop);
+
+ /**
+ * Get dirty flag.
+ *
+ * @return if true, the logical screen is not in sync with the physical
+ * screen
+ */
+ public boolean isDirty();
+
+ /**
+ * Get the attributes at one location.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @return attributes at (x, y)
+ */
+ public CellAttributes getAttrXY(final int x, final int y);
+
+ /**
+ * Get the cell at one location.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @return the character + attributes
+ */
+ public Cell getCharXY(final int x, final int y);
+
+ /**
+ * Set the attributes at one location.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param attr attributes to use (bold, foreColor, backColor)
+ */
+ public void putAttrXY(final int x, final int y,
+ final CellAttributes attr);
+
+ /**
+ * Set the attributes at one location.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param attr attributes to use (bold, foreColor, backColor)
+ * @param clip if true, honor clipping/offset
+ */
+ public void putAttrXY(final int x, final int y,
+ final CellAttributes attr, final boolean clip);
+
+ /**
+ * Fill the entire screen with one character with attributes.
+ *
+ * @param ch character to draw
+ * @param attr attributes to use (bold, foreColor, backColor)
+ */
+ public void putAll(final int ch, final CellAttributes attr);
+
+ /**
+ * Render one character with attributes.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param ch character + attributes to draw
+ */
+ public void putCharXY(final int x, final int y, final Cell ch);
+
+ /**
+ * Render one character with attributes.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param ch character to draw
+ * @param attr attributes to use (bold, foreColor, backColor)
+ */
+ public void putCharXY(final int x, final int y, final int ch,
+ final CellAttributes attr);
+
+ /**
+ * Render one character without changing the underlying attributes.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param ch character to draw
+ */
+ public void putCharXY(final int x, final int y, final int ch);
+
+ /**
+ * Render a string. Does not wrap if the string exceeds the line.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param str string to draw
+ * @param attr attributes to use (bold, foreColor, backColor)
+ */
+ public void putStringXY(final int x, final int y, final String str,
+ final CellAttributes attr);
+
+ /**
+ * Render a string without changing the underlying attribute. Does not
+ * wrap if the string exceeds the line.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param str string to draw
+ */
+ public void putStringXY(final int x, final int y, final String str);
+
+ /**
+ * Draw a vertical line from (x, y) to (x, y + n).
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param n number of characters to draw
+ * @param ch character to draw
+ * @param attr attributes to use (bold, foreColor, backColor)
+ */
+ public void vLineXY(final int x, final int y, final int n,
+ final int ch, final CellAttributes attr);
+
+ /**
+ * Draw a horizontal line from (x, y) to (x + n, y).
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param n number of characters to draw
+ * @param ch character to draw
+ * @param attr attributes to use (bold, foreColor, backColor)
+ */
+ public void hLineXY(final int x, final int y, final int n,
+ final int ch, final CellAttributes attr);
+
+ /**
+ * Change the width. Everything on-screen will be destroyed and must be
+ * redrawn.
+ *
+ * @param width new screen width
+ */
+ public void setWidth(final int width);
+
+ /**
+ * Change the height. Everything on-screen will be destroyed and must be
+ * redrawn.
+ *
+ * @param height new screen height
+ */
+ public void setHeight(final int height);
+
+ /**
+ * Change the width and height. Everything on-screen will be destroyed
+ * and must be redrawn.
+ *
+ * @param width new screen width
+ * @param height new screen height
+ */
+ public void setDimensions(final int width, final int height);
+
+ /**
+ * Get the height.
+ *
+ * @return current screen height
+ */
+ public int getHeight();
+
+ /**
+ * Get the width.
+ *
+ * @return current screen width
+ */
+ public int getWidth();
+
+ /**
+ * Reset screen to not-bold, white-on-black. Also flushes the offset and
+ * clip variables.
+ */
+ public void reset();
+
+ /**
+ * Flush the offset and clip variables.
+ */
+ public void resetClipping();
+
+ /**
+ * Clear the logical screen.
+ */
+ public void clear();
+
+ /**
+ * Draw a box with a border and empty background.
+ *
+ * @param left left column of box. 0 is the left-most row.
+ * @param top top row of the box. 0 is the top-most row.
+ * @param right right column of box
+ * @param bottom bottom row of the box
+ * @param border attributes to use for the border
+ * @param background attributes to use for the background
+ */
+ public void drawBox(final int left, final int top,
+ final int right, final int bottom,
+ final CellAttributes border, final CellAttributes background);
+
+ /**
+ * Draw a box with a border and empty background.
+ *
+ * @param left left column of box. 0 is the left-most row.
+ * @param top top row of the box. 0 is the top-most row.
+ * @param right right column of box
+ * @param bottom bottom row of the box
+ * @param border attributes to use for the border
+ * @param background attributes to use for the background
+ * @param borderType if 1, draw a single-line border; if 2, draw a
+ * double-line border; if 3, draw double-line top/bottom edges and
+ * single-line left/right edges (like Qmodem)
+ * @param shadow if true, draw a "shadow" on the box
+ */
+ public void drawBox(final int left, final int top,
+ final int right, final int bottom,
+ final CellAttributes border, final CellAttributes background,
+ final int borderType, final boolean shadow);
+
+ /**
+ * Draw a box shadow.
+ *
+ * @param left left column of box. 0 is the left-most row.
+ * @param top top row of the box. 0 is the top-most row.
+ * @param right right column of box
+ * @param bottom bottom row of the box
+ */
+ public void drawBoxShadow(final int left, final int top,
+ final int right, final int bottom);
+
+ /**
+ * Clear the physical screen.
+ */
+ public void clearPhysical();
+
+ /**
+ * Unset every image cell on one row of the physical screen, forcing
+ * images on that row to be redrawn.
+ *
+ * @param y row coordinate. 0 is the top-most row.
+ */
+ public void unsetImageRow(final int y);
+
+ /**
+ * Classes must provide an implementation to push the logical screen to
+ * the physical device.
+ */
+ public void flushPhysical();
+
+ /**
+ * Put the cursor at (x,y).
+ *
+ * @param visible if true, the cursor should be visible
+ * @param x column coordinate to put the cursor on
+ * @param y row coordinate to put the cursor on
+ */
+ public void putCursor(final boolean visible, final int x, final int y);
+
+ /**
+ * Hide the cursor.
+ */
+ public void hideCursor();
+
+ /**
+ * Get the cursor visibility.
+ *
+ * @return true if the cursor is visible
+ */
+ public boolean isCursorVisible();
+
+ /**
+ * Get the cursor X position.
+ *
+ * @return the cursor x column position
+ */
+ public int getCursorX();
+
+ /**
+ * Get the cursor Y position.
+ *
+ * @return the cursor y row position
+ */
+ public int getCursorY();
+
+ /**
+ * Set the window title.
+ *
+ * @param title the new title
+ */
+ public void setTitle(final String title);
+
+ /**
+ * Get the width of a character cell in pixels.
+ *
+ * @return the width in pixels of a character cell
+ */
+ public int getTextWidth();
+
+ /**
+ * Get the height of a character cell in pixels.
+ *
+ * @return the height in pixels of a character cell
+ */
+ public int getTextHeight();
+
+ }
--- /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;
+
+ /**
+ * SessionInfo is used to store per-session properties that are determined at
+ * different layers of the communication stack.
+ */
+ public interface SessionInfo {
+
+ /**
+ * Username getter.
+ *
+ * @return the username
+ */
+ public String getUsername();
+
+ /**
+ * Username setter.
+ *
+ * @param username the value
+ */
+ public void setUsername(String username);
+
+ /**
+ * Language getter.
+ *
+ * @return the language
+ */
+ public String getLanguage();
+
+ /**
+ * Language setter.
+ *
+ * @param language the value
+ */
+ public void setLanguage(String language);
+
+ /**
+ * Text window width getter.
+ *
+ * @return the window width
+ */
+ public int getWindowWidth();
+
+ /**
+ * Text window height getter.
+ *
+ * @return the window height
+ */
+ public int getWindowHeight();
+
+ /**
+ * Re-query the text window size.
+ */
+ public void queryWindowSize();
+ }
--- /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.awt.Font;
+ import javax.swing.JComponent;
+
+ /**
+ * This class uses standard Swing calls to handle screen, keyboard, and mouse
+ * I/O.
+ */
+ public class SwingBackend extends GenericBackend {
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor. The window will be 80x25 with font size 20 pts.
+ */
+ public SwingBackend() {
+ this(null, 80, 25, 20);
+ }
+
+ /**
+ * Public constructor. The window will be 80x25 with font size 20 pts.
+ *
+ * @param listener the object this backend needs to wake up when new
+ * input comes in
+ */
+ public SwingBackend(final Object listener) {
+ this(listener, 80, 25, 20);
+ }
+
+ /**
+ * Public constructor will spawn a new JFrame with font size 20 pts.
+ *
+ * @param windowWidth the number of text columns to start with
+ * @param windowHeight the number of text rows to start with
+ */
+ public SwingBackend(final int windowWidth, final int windowHeight) {
+ this(null, windowWidth, windowHeight, 20);
+ }
+
+ /**
+ * Public constructor will spawn a new JFrame.
+ *
+ * @param windowWidth the number of text columns to start with
+ * @param windowHeight the number of text rows to start with
+ * @param fontSize the size in points. Good values to pick are: 16, 20,
+ * 22, and 24.
+ */
+ public SwingBackend(final int windowWidth, final int windowHeight,
+ final int fontSize) {
+
+ this(null, windowWidth, windowHeight, fontSize);
+ }
+
+ /**
+ * Public constructor will spawn a new JFrame.
+ *
+ * @param listener the object this backend needs to wake up when new
+ * input comes in
+ * @param windowWidth the number of text columns to start with
+ * @param windowHeight the number of text rows to start with
+ * @param fontSize the size in points. Good values to pick are: 16, 20,
+ * 22, and 24.
+ */
+ public SwingBackend(final Object listener, final int windowWidth,
+ final int windowHeight, final int fontSize) {
+
+ // Create a Swing backend using a JFrame
+ terminal = new SwingTerminal(windowWidth, windowHeight, fontSize,
+ listener);
+
+ // Hang onto the session info
+ this.sessionInfo = ((SwingTerminal) terminal).getSessionInfo();
+
+ // SwingTerminal is the screen too
+ screen = (SwingTerminal) terminal;
+ }
+
+ /**
+ * Public constructor will render onto a JComponent.
+ *
+ * @param component the Swing component to render to
+ * @param listener the object this backend needs to wake up when new
+ * input comes in
+ * @param windowWidth the number of text columns to start with
+ * @param windowHeight the number of text rows to start with
+ * @param fontSize the size in points. Good values to pick are: 16, 20,
+ * 22, and 24.
+ */
+ public SwingBackend(final JComponent component, final Object listener,
+ final int windowWidth, final int windowHeight, final int fontSize) {
+
+ // Create a Swing backend using a JComponent
+ terminal = new SwingTerminal(component, windowWidth, windowHeight,
+ fontSize, listener);
+
+ // Hang onto the session info
+ this.sessionInfo = ((SwingTerminal) terminal).getSessionInfo();
+
+ // SwingTerminal is the screen too
+ screen = (SwingTerminal) terminal;
+ }
+
+ // ------------------------------------------------------------------------
+ // SwingBackend -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Set to a new font, and resize the screen to match its dimensions.
+ *
+ * @param font the new font
+ */
+ public void setFont(final Font font) {
+ ((SwingTerminal) terminal).setFont(font);
+ }
+
+ /**
+ * Get the number of millis to wait before switching the blink from
+ * visible to invisible.
+ *
+ * @return the number of milli to wait before switching the blink from
+ * visible to invisible
+ */
+ public long getBlinkMillis() {
+ return ((SwingTerminal) terminal).getBlinkMillis();
+ }
+
+ /**
+ * Getter for the underlying Swing component.
+ *
+ * @return the SwingComponent
+ */
+ public SwingComponent getSwingComponent() {
+ return ((SwingTerminal) terminal).getSwingComponent();
+ }
+
+ }
--- /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.awt.Color;
+ import java.awt.Cursor;
+ import java.awt.Font;
+ import java.awt.Graphics;
+ import java.awt.Insets;
+ import java.awt.Point;
+ import java.awt.Toolkit;
+ import java.awt.event.ComponentListener;
+ import java.awt.event.KeyListener;
+ import java.awt.event.MouseListener;
+ import java.awt.event.MouseMotionListener;
+ import java.awt.event.MouseWheelListener;
+ import java.awt.event.WindowListener;
+ import java.awt.image.BufferedImage;
+ import java.awt.image.BufferStrategy;
+ import java.io.IOException;
+ import javax.imageio.ImageIO;
+ import javax.swing.JComponent;
+ import javax.swing.JFrame;
+ import javax.swing.SwingUtilities;
+
+ /**
+ * Wrapper for integrating with Swing, because JFrame and JComponent have
+ * separate hierarchies.
+ */
+ class SwingComponent {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * If true, use triple buffering when drawing to a JFrame.
+ */
+ public static boolean tripleBuffer = true;
+
+ /**
+ * The frame reference, if we are drawing to a JFrame.
+ */
+ private JFrame frame;
+
+ /**
+ * The component reference, if we are drawing to a JComponent.
+ */
+ private JComponent component;
+
+ /**
+ * An optional border in pixels to add.
+ */
+ private static final int BORDER = 1;
+
+ /**
+ * 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);
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Construct using a JFrame.
+ *
+ * @param frame the JFrame to draw to
+ */
+ public SwingComponent(final JFrame frame) {
+ this.frame = frame;
+ setupFrame();
+ }
+
+ /**
+ * Construct using a JComponent.
+ *
+ * @param component the JComponent to draw to
+ */
+ public SwingComponent(final JComponent component) {
+ this.component = component;
+ setupComponent();
+ }
+
+ // ------------------------------------------------------------------------
+ // SwingComponent ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get the BufferStrategy object needed for triple-buffering.
+ *
+ * @return the BufferStrategy
+ * @throws IllegalArgumentException if this function is called when
+ * not rendering to a JFrame
+ */
+ public BufferStrategy getBufferStrategy() {
+ if (frame != null) {
+ return frame.getBufferStrategy();
+ } else {
+ throw new IllegalArgumentException("BufferStrategy not used " +
+ "for JComponent access");
+ }
+ }
+
+ /**
+ * Get the JFrame reference.
+ *
+ * @return the frame, or null if this is drawing to a JComponent
+ */
+ public JFrame getFrame() {
+ return frame;
+ }
+
+ /**
+ * Get the JComponent reference.
+ *
+ * @return the component, or null if this is drawing to a JFrame
+ */
+ public JComponent getComponent() {
+ return component;
+ }
+
+ /**
+ * Setup to render to an existing JComponent.
+ */
+ public void setupComponent() {
+ component.setBackground(Color.black);
+
+ if (System.getProperty("jexer.Swing.mouseImage") != null) {
+ component.setCursor(getMouseImage());
+ } else if (System.getProperty("jexer.Swing.mouseStyle") != null) {
+ component.setCursor(getMouseCursor());
+ } else if (System.getProperty("jexer.textMouse",
+ "true").equals("false")
+ ) {
+ // If the user has suppressed the text mouse, don't kill the X11
+ // mouse.
+ component.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
+ } else {
+ // Kill the X11 cursor
+ // Transparent 16 x 16 pixel cursor image.
+ BufferedImage cursorImg = new BufferedImage(16, 16,
+ BufferedImage.TYPE_INT_ARGB);
+ // Create a new blank cursor.
+ Cursor blankCursor = Toolkit.getDefaultToolkit().createCustomCursor(
+ cursorImg, new Point(0, 0), "blank cursor");
+ component.setCursor(blankCursor);
+ }
+
+ // Be capable of seeing Tab / Shift-Tab
+ component.setFocusTraversalKeysEnabled(false);
+ }
+
+ /**
+ * Setup to render to an existing JFrame.
+ */
+ public void setupFrame() {
+ frame.setTitle("Jexer Application");
+ frame.setBackground(Color.black);
+ frame.pack();
+
+ if (System.getProperty("jexer.Swing.mouseImage") != null) {
+ frame.setCursor(getMouseImage());
+ } else if (System.getProperty("jexer.Swing.mouseStyle") != null) {
+ frame.setCursor(getMouseCursor());
+ } else if (System.getProperty("jexer.textMouse",
+ "true").equals("false")
+ ) {
+ // If the user has suppressed the text mouse, don't kill the X11
+ // mouse.
+ frame.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
+ } else {
+ // Kill the X11 cursor
+ // Transparent 16 x 16 pixel cursor image.
+ BufferedImage cursorImg = new BufferedImage(16, 16,
+ BufferedImage.TYPE_INT_ARGB);
+ // Create a new blank cursor.
+ Cursor blankCursor = Toolkit.getDefaultToolkit().createCustomCursor(
+ cursorImg, new Point(0, 0), "blank cursor");
+ frame.setCursor(blankCursor);
+ }
+
+ // Be capable of seeing Tab / Shift-Tab
+ frame.setFocusTraversalKeysEnabled(false);
+
+ // Setup triple-buffering
+ if (tripleBuffer) {
+ frame.setIgnoreRepaint(true);
+ frame.createBufferStrategy(3);
+ }
+ }
+
+ /**
+ * Load an image named in jexer.Swing.mouseImage as the mouse cursor.
+ * The image must be on the classpath.
+ *
+ * @return the cursor
+ */
+ private Cursor getMouseImage() {
+ Cursor cursor = Cursor.getDefaultCursor();
+ String filename = System.getProperty("jexer.Swing.mouseImage");
+ assert (filename != null);
+
+ try {
+ ClassLoader loader = Thread.currentThread().
+ getContextClassLoader();
+
+ java.net.URL url = loader.getResource(filename);
+ if (url == null) {
+ // User named a file, but it's not on the classpath. Bail
+ // out.
+ return cursor;
+ }
+
+ BufferedImage cursorImage = ImageIO.read(url);
+ java.awt.Dimension cursorSize = Toolkit.getDefaultToolkit().
+ getBestCursorSize(
+ cursorImage.getWidth(), cursorImage.getHeight());
+
+ cursor = Toolkit.getDefaultToolkit().createCustomCursor(cursorImage,
+ new Point((int) Math.min(cursorImage.getWidth() / 2,
+ cursorSize.getWidth() - 1),
+ (int) Math.min(cursorImage.getHeight() / 2,
+ cursorSize.getHeight() - 1)),
+ "custom cursor");
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ return cursor;
+ }
+
+ /**
+ * Get the appropriate mouse cursor based on jexer.Swing.mouseStyle.
+ *
+ * @return the cursor
+ */
+ private Cursor getMouseCursor() {
+ Cursor cursor = Cursor.getDefaultCursor();
+ String style = System.getProperty("jexer.Swing.mouseStyle");
+ assert (style != null);
+
+ style = style.toLowerCase();
+
+ if (style.equals("none")) {
+ // Transparent 16 x 16 pixel cursor image.
+ BufferedImage cursorImg = new BufferedImage(16, 16,
+ BufferedImage.TYPE_INT_ARGB);
+ // Create a new blank cursor.
+ cursor = Toolkit.getDefaultToolkit().createCustomCursor(
+ cursorImg, new Point(0, 0), "blank cursor");
+ } else if (style.equals("default")) {
+ cursor = Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR);
+ } else if (style.equals("hand")) {
+ cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
+ } else if (style.equals("text")) {
+ cursor = Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR);
+ } else if (style.equals("move")) {
+ cursor = Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
+ } else if (style.equals("crosshair")) {
+ cursor = Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR);
+ }
+
+ return cursor;
+ }
+
+ /**
+ * Set the window title.
+ *
+ * @param title the new title
+ */
+ public void setTitle(final String title) {
+ if (frame != null) {
+ frame.setTitle(title);
+ }
+ }
+
+ /**
+ * Paints this component.
+ *
+ * @param g the graphics context to use for painting
+ */
+ public void paint(Graphics g) {
+ if (frame != null) {
+ frame.paint(g);
+ } else {
+ component.paint(g);
+ }
+ }
+
+ /**
+ * Repaints this component.
+ */
+ public void repaint() {
+ if (frame != null) {
+ frame.repaint();
+ } else {
+ component.repaint();
+ }
+ }
+
+ /**
+ * Repaints the specified rectangle of this component.
+ *
+ * @param x the x coordinate
+ * @param y the y coordinate
+ * @param width the width
+ * @param height the height
+ */
+ public void repaint(int x, int y, int width, int height) {
+ if (frame != null) {
+ frame.repaint(x, y, width, height);
+ } else {
+ component.repaint(x, y, width, height);
+ }
+ }
+
+ /**
+ * If a border has been set on this component, returns the border's
+ * insets; otherwise calls super.getInsets.
+ *
+ * @return the value of the insets property
+ */
+ public Insets getInsets() {
+ Insets swingInsets = null;
+ if (frame != null) {
+ swingInsets = frame.getInsets();
+ } else {
+ swingInsets = component.getInsets();
+ }
+ Insets result = new Insets(swingInsets.top + adjustInsets.top,
+ swingInsets.left + adjustInsets.left,
+ swingInsets.bottom + adjustInsets.bottom,
+ swingInsets.right + adjustInsets.right);
+ return result;
+ }
+
+ /**
+ * Returns the current width of this component.
+ *
+ * @return the current width of this component
+ */
+ public int getWidth() {
+ if (frame != null) {
+ return frame.getWidth();
+ } else {
+ return component.getWidth();
+ }
+ }
+
+ /**
+ * Returns the current height of this component.
+ *
+ * @return the current height of this component
+ */
+ public int getHeight() {
+ if (frame != null) {
+ return frame.getHeight();
+ } else {
+ return component.getHeight();
+ }
+ }
+
+ /**
+ * Gets the font of this component.
+ *
+ * @return this component's font; if a font has not been set for this
+ * component, the font of its parent is returned
+ */
+ public Font getFont() {
+ if (frame != null) {
+ return frame.getFont();
+ } else {
+ return component.getFont();
+ }
+ }
+
+ /**
+ * Sets the font of this component.
+ *
+ * @param f the font to become this component's font; if this parameter
+ * is null then this component will inherit the font of its parent
+ */
+ public void setFont(final Font f) {
+ if (frame != null) {
+ frame.setFont(f);
+ } else {
+ component.setFont(f);
+ }
+ }
+
+ /**
+ * Shows or hides this Window depending on the value of parameter b.
+ *
+ * @param b if true, make visible, else make invisible
+ */
+ public void setVisible(final boolean b) {
+ if (frame != null) {
+ frame.setVisible(b);
+ } else {
+ component.setVisible(b);
+ }
+ }
+
+ /**
+ * Creates a graphics context for this component. This method will return
+ * null if this component is currently not displayable.
+ *
+ * @return a graphics context for this component, or null if it has none
+ */
+ public Graphics getGraphics() {
+ if (frame != null) {
+ return frame.getGraphics();
+ } else {
+ return component.getGraphics();
+ }
+ }
+
+ /**
+ * Releases all of the native screen resources used by this Window, its
+ * subcomponents, and all of its owned children. That is, the resources
+ * for these Components will be destroyed, any memory they consume will
+ * be returned to the OS, and they will be marked as undisplayable.
+ */
+ public void dispose() {
+ if (frame != null) {
+ frame.dispose();
+ } else {
+ component.getParent().remove(component);
+ }
+ }
+
+ /**
+ * Resize the component to match the font dimensions.
+ *
+ * @param width the new width in pixels
+ * @param height the new height in pixels
+ */
+ public void setDimensions(final int width, final int height) {
+ if (SwingUtilities.isEventDispatchThread()) {
+ // We are in the Swing thread and can safely set the size.
+
+ // Figure out the thickness of borders and use that to set the
+ // final size.
+ if (frame != null) {
+ Insets insets = getInsets();
+ frame.setSize(width + insets.left + insets.right,
+ height + insets.top + insets.bottom);
+ } else {
+ Insets insets = getInsets();
+ component.setSize(width + insets.left + insets.right,
+ height + insets.top + insets.bottom);
+ }
+ return;
+ }
+
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ // Figure out the thickness of borders and use that to set
+ // the final size.
+ if (frame != null) {
+ Insets insets = getInsets();
+ frame.setSize(width + insets.left + insets.right,
+ height + insets.top + insets.bottom);
+ } else {
+ Insets insets = getInsets();
+ component.setSize(width + insets.left + insets.right,
+ height + insets.top + insets.bottom);
+ }
+ }
+ });
+ }
+
+ /**
+ * Adds the specified component listener to receive component events from
+ * this component. If listener l is null, no exception is thrown and no
+ * action is performed.
+ *
+ * @param l the component listener
+ */
+ public void addComponentListener(ComponentListener l) {
+ if (frame != null) {
+ frame.addComponentListener(l);
+ } else {
+ component.addComponentListener(l);
+ }
+ }
+
+ /**
+ * Adds the specified key listener to receive key events from this
+ * component. If l is null, no exception is thrown and no action is
+ * performed.
+ *
+ * @param l the key listener.
+ */
+ public void addKeyListener(KeyListener l) {
+ if (frame != null) {
+ frame.addKeyListener(l);
+ } else {
+ component.addKeyListener(l);
+ }
+ }
+
+ /**
+ * Adds the specified mouse listener to receive mouse events from this
+ * component. If listener l is null, no exception is thrown and no action
+ * is performed.
+ *
+ * @param l the mouse listener
+ */
+ public void addMouseListener(MouseListener l) {
+ if (frame != null) {
+ frame.addMouseListener(l);
+ } else {
+ component.addMouseListener(l);
+ }
+ }
+
+ /**
+ * Adds the specified mouse motion listener to receive mouse motion
+ * events from this component. If listener l is null, no exception is
+ * thrown and no action is performed.
+ *
+ * @param l the mouse motion listener
+ */
+ public void addMouseMotionListener(MouseMotionListener l) {
+ if (frame != null) {
+ frame.addMouseMotionListener(l);
+ } else {
+ component.addMouseMotionListener(l);
+ }
+ }
+
+ /**
+ * Adds the specified mouse wheel listener to receive mouse wheel events
+ * from this component. Containers also receive mouse wheel events from
+ * sub-components.
+ *
+ * @param l the mouse wheel listener
+ */
+ public void addMouseWheelListener(MouseWheelListener l) {
+ if (frame != null) {
+ frame.addMouseWheelListener(l);
+ } else {
+ component.addMouseWheelListener(l);
+ }
+ }
+
+ /**
+ * Adds the specified window listener to receive window events from this
+ * window. If l is null, no exception is thrown and no action is
+ * performed.
+ *
+ * @param l the window listener
+ */
+ public void addWindowListener(WindowListener l) {
+ if (frame != null) {
+ frame.addWindowListener(l);
+ }
+ }
+
+ /**
+ * Requests that this Component get the input focus, if this Component's
+ * top-level ancestor is already the focused Window.
+ */
+ public void requestFocusInWindow() {
+ if (frame != null) {
+ frame.requestFocusInWindow();
+ } else {
+ component.requestFocusInWindow();
+ }
+ }
+
+ }
--- /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.awt.Insets;
+
+ /**
+ * SwingSessionInfo provides a session implementation with a callback into
+ * Swing to support queryWindowSize(). The username is blank, language is
+ * "en_US", with a 80x25 text window.
+ */
+ public class SwingSessionInfo implements SessionInfo {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The Swing JFrame or JComponent.
+ */
+ private SwingComponent swing;
+
+ /**
+ * The width of a text cell in pixels.
+ */
+ private int textWidth = 10;
+
+ /**
+ * The height of a text cell in pixels.
+ */
+ private int textHeight = 10;
+
+ /**
+ * User name.
+ */
+ private String username = "";
+
+ /**
+ * Language.
+ */
+ private String language = "en_US";
+
+ /**
+ * Text window width.
+ */
+ private int windowWidth = 80;
+
+ /**
+ * Text window height.
+ */
+ private int windowHeight = 25;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param swing the Swing JFrame or JComponent
+ * @param textWidth the width of a cell in pixels
+ * @param textHeight the height of a cell in pixels
+ */
+ public SwingSessionInfo(final SwingComponent swing, final int textWidth,
+ final int textHeight) {
+
+ this.swing = swing;
+ this.textWidth = textWidth;
+ this.textHeight = textHeight;
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param swing the Swing JFrame or JComponent
+ * @param textWidth the width of a cell in pixels
+ * @param textHeight the height of a cell in pixels
+ * @param width the number of columns
+ * @param height the number of rows
+ */
+ public SwingSessionInfo(final SwingComponent swing, final int textWidth,
+ final int textHeight, final int width, final int height) {
+
+ this.swing = swing;
+ this.textWidth = textWidth;
+ this.textHeight = textHeight;
+ this.windowWidth = width;
+ this.windowHeight = height;
+ }
+
+ // ------------------------------------------------------------------------
+ // SessionInfo ------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Username getter.
+ *
+ * @return the username
+ */
+ public String getUsername() {
+ return this.username;
+ }
+
+ /**
+ * Username setter.
+ *
+ * @param username the value
+ */
+ public void setUsername(final String username) {
+ this.username = username;
+ }
+
+ /**
+ * Language getter.
+ *
+ * @return the language
+ */
+ public String getLanguage() {
+ return this.language;
+ }
+
+ /**
+ * Language setter.
+ *
+ * @param language the value
+ */
+ public void setLanguage(final String language) {
+ this.language = language;
+ }
+
+ /**
+ * Text window width getter.
+ *
+ * @return the window width
+ */
+ public int getWindowWidth() {
+ return windowWidth;
+ }
+
+ /**
+ * Text window height getter.
+ *
+ * @return the window height
+ */
+ public int getWindowHeight() {
+ return windowHeight;
+ }
+
+ /**
+ * Re-query the text window size.
+ */
+ public void queryWindowSize() {
+ Insets insets = swing.getInsets();
+ int width = swing.getWidth() - insets.left - insets.right;
+ int height = swing.getHeight() - insets.top - insets.bottom;
+ // In theory, if Java reported pixel-perfect dimensions, the
+ // expressions above would precisely line up with the requested
+ // window size from SwingComponent.setDimensions(). In practice,
+ // there appears to be a small difference. Add half a text cell in
+ // both directions before the division to hopefully reach the same
+ // result as setDimensions() was supposed to give us.
+ width += (textWidth / 2);
+ height += (textHeight / 2);
+ windowWidth = width / textWidth;
+ windowHeight = height / textHeight;
+
+ /*
+ System.err.printf("queryWindowSize(): frame %d %d window %d %d\n",
+ swing.getWidth(), swing.getHeight(),
+ windowWidth, windowHeight);
+ */
+ }
+
+ // ------------------------------------------------------------------------
+ // SwingSessionInfo -------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Set the dimensions of a single text cell.
+ *
+ * @param textWidth the width of a cell in pixels
+ * @param textHeight the height of a cell in pixels
+ */
+ public void setTextCellDimensions(final int textWidth,
+ final int textHeight) {
+
+ this.textWidth = textWidth;
+ this.textHeight = textHeight;
+ }
+
+ /**
+ * Getter for the underlying Swing component.
+ *
+ * @return the SwingComponent
+ */
+ public SwingComponent getSwingComponent() {
+ return swing;
+ }
+
+ }
--- /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.awt.BorderLayout;
+ import java.awt.Color;
+ import java.awt.Font;
+ import java.awt.FontMetrics;
+ import java.awt.Graphics2D;
+ import java.awt.Graphics;
+ import java.awt.Insets;
+ import java.awt.Rectangle;
+ import java.awt.Toolkit;
+ import java.awt.event.ComponentEvent;
+ import java.awt.event.ComponentListener;
+ import java.awt.event.KeyEvent;
+ import java.awt.event.KeyListener;
+ import java.awt.event.MouseEvent;
+ import java.awt.event.MouseListener;
+ import java.awt.event.MouseMotionListener;
+ import java.awt.event.MouseWheelEvent;
+ import java.awt.event.MouseWheelListener;
+ import java.awt.event.WindowEvent;
+ import java.awt.event.WindowListener;
+ import java.awt.geom.Rectangle2D;
+ import java.awt.image.BufferedImage;
+ import java.io.InputStream;
+ import java.util.ArrayList;
+ import java.util.HashMap;
+ import java.util.List;
+ import java.util.Map;
+ import javax.swing.JComponent;
+ import javax.swing.JFrame;
+ import javax.swing.ImageIcon;
+ import javax.swing.SwingUtilities;
+
+ import jexer.TKeypress;
+ import jexer.bits.Cell;
+ import jexer.bits.CellAttributes;
+ import jexer.event.TCommandEvent;
+ import jexer.event.TInputEvent;
+ import jexer.event.TKeypressEvent;
+ import jexer.event.TMouseEvent;
+ import jexer.event.TResizeEvent;
+ import static jexer.TCommand.*;
+ import static jexer.TKeypress.*;
+
+ /**
+ * This Screen backend reads keystrokes and mouse events and draws to either
+ * a Java Swing JFrame (potentially triple-buffered) or a JComponent.
+ *
+ * This class is a bit of an inversion of typical GUI classes. It performs
+ * all of the drawing logic from SwingTerminal (which is not a Swing class),
+ * and uses a SwingComponent wrapper class to call the JFrame or JComponent
+ * methods.
+ */
+ public class SwingTerminal extends LogicalScreen
+ implements TerminalReader,
+ ComponentListener, KeyListener,
+ MouseListener, MouseMotionListener,
+ MouseWheelListener, WindowListener {
+
+ // ------------------------------------------------------------------------
+ // Constants --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The icon image location.
+ */
+ private static final String ICONFILE = "jexer_logo_128.png";
+
+ /**
+ * The terminus font resource filename.
+ */
+ public static final String FONTFILE = "terminus-ttf-4.39/TerminusTTF-Bold-4.39.ttf";
+
+ /**
+ * Cursor style to draw.
+ */
+ public enum CursorStyle {
+ /**
+ * Use an underscore for the cursor.
+ */
+ UNDERLINE,
+
+ /**
+ * Use a solid block for the cursor.
+ */
+ BLOCK,
+
+ /**
+ * Use an outlined block for the cursor.
+ */
+ OUTLINE,
+
+ /**
+ * Use a vertical bar for the cursor.
+ */
+ VERTICAL_BAR,
+ }
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ // Colors to map DOS colors to AWT colors.
+ private static Color MYBLACK;
+ private static Color MYRED;
+ private static Color MYGREEN;
+ private static Color MYYELLOW;
+ private static Color MYBLUE;
+ private static Color MYMAGENTA;
+ private static Color MYCYAN;
+ private static Color MYWHITE;
+ private static Color MYBOLD_BLACK;
+ private static Color MYBOLD_RED;
+ private static Color MYBOLD_GREEN;
+ private static Color MYBOLD_YELLOW;
+ private static Color MYBOLD_BLUE;
+ private static Color MYBOLD_MAGENTA;
+ private static Color MYBOLD_CYAN;
+ private static Color MYBOLD_WHITE;
+
+ /**
+ * When true, all the MYBLACK, MYRED, etc. colors are set.
+ */
+ private static boolean dosColors = false;
+
+ /**
+ * The Swing component or frame to draw to.
+ */
+ private SwingComponent swing;
+
+ /**
+ * A cache of previously-rendered glyphs for blinking text, when it is
+ * not visible.
+ */
+ private Map<Cell, BufferedImage> glyphCacheBlink;
+
+ /**
+ * A cache of previously-rendered glyphs for non-blinking, or
+ * blinking-and-visible, text.
+ */
+ private Map<Cell, BufferedImage> glyphCache;
+
+ /**
+ * If true, we were successful at getting the font dimensions.
+ */
+ private boolean gotFontDimensions = false;
+
+ /**
+ * The currently selected font.
+ */
+ private Font font = null;
+
+ /**
+ * The currently selected font size in points.
+ */
+ private int fontSize = 16;
+
+ /**
+ * Width of a character cell in pixels.
+ */
+ private int textWidth = 16;
+
+ /**
+ * Height of a character cell in pixels.
+ */
+ private int textHeight = 20;
+
+ /**
+ * Width of a character cell in pixels, as reported by font.
+ */
+ private int fontTextWidth = 1;
+
+ /**
+ * Height of a character cell in pixels, as reported by font.
+ */
+ private int fontTextHeight = 1;
+
+ /**
+ * Descent of a character cell in pixels.
+ */
+ private int maxDescent = 0;
+
+ /**
+ * System-dependent Y adjustment for text in the character cell.
+ */
+ private int textAdjustY = 0;
+
+ /**
+ * System-dependent X adjustment for text in the character cell.
+ */
+ private int textAdjustX = 0;
+
+ /**
+ * System-dependent height adjustment for text in the character cell.
+ */
+ private int textAdjustHeight = 0;
+
+ /**
+ * System-dependent width adjustment for text in the character cell.
+ */
+ private int textAdjustWidth = 0;
+
+ /**
+ * Top pixel absolute location.
+ */
+ private int top = 30;
+
+ /**
+ * Left pixel absolute location.
+ */
+ private int left = 30;
+
+ /**
+ * The cursor style to draw.
+ */
+ private CursorStyle cursorStyle = CursorStyle.UNDERLINE;
+
+ /**
+ * The number of millis to wait before switching the blink from visible
+ * to invisible. Set to 0 or negative to disable blinking.
+ */
+ private long blinkMillis = 500;
+
+ /**
+ * If true, the cursor should be visible right now based on the blink
+ * time.
+ */
+ private boolean cursorBlinkVisible = true;
+
+ /**
+ * The time that the blink last flipped from visible to invisible or
+ * from invisible to visible.
+ */
+ private long lastBlinkTime = 0;
+
+ /**
+ * The session information.
+ */
+ private SwingSessionInfo sessionInfo;
+
+ /**
+ * The listening object that run() wakes up on new input.
+ */
+ private Object listener;
+
+ /**
+ * The event queue, filled up by a thread reading on input.
+ */
+ private List<TInputEvent> eventQueue;
+
+ /**
+ * The last reported mouse X position.
+ */
+ private int oldMouseX = -1;
+
+ /**
+ * The last reported mouse Y position.
+ */
+ private int oldMouseY = -1;
+
+ /**
+ * true if mouse1 was down. Used to report mouse1 on the release event.
+ */
+ private boolean mouse1 = false;
+
+ /**
+ * true if mouse2 was down. Used to report mouse2 on the release event.
+ */
+ private boolean mouse2 = false;
+
+ /**
+ * true if mouse3 was down. Used to report mouse3 on the release event.
+ */
+ private boolean mouse3 = false;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Static constructor.
+ */
+ static {
+ setDOSColors();
+ }
+
+ /**
+ * Public constructor creates a new JFrame to render to.
+ *
+ * @param windowWidth the number of text columns to start with
+ * @param windowHeight the number of text rows to start with
+ * @param fontSize the size in points. Good values to pick are: 16, 20,
+ * 22, and 24.
+ * @param listener the object this backend needs to wake up when new
+ * input comes in
+ */
+ public SwingTerminal(final int windowWidth, final int windowHeight,
+ final int fontSize, final Object listener) {
+
+ this.fontSize = fontSize;
+
+ reloadOptions();
+
+ try {
+ SwingUtilities.invokeAndWait(new Runnable() {
+ public void run() {
+
+ JFrame frame = new JFrame() {
+
+ /**
+ * Serializable version.
+ */
+ private static final long serialVersionUID = 1;
+
+ /**
+ * The code that performs the actual drawing.
+ */
+ public SwingTerminal screen = null;
+
+ /*
+ * Anonymous class initializer saves the screen
+ * reference, so that paint() and the like call out
+ * to SwingTerminal.
+ */
+ {
+ this.screen = SwingTerminal.this;
+ }
+
+ /**
+ * Update redraws the whole screen.
+ *
+ * @param gr the Swing Graphics context
+ */
+ @Override
+ public void update(final Graphics gr) {
+ // The default update clears the area. Don't do
+ // that, instead just paint it directly.
+ paint(gr);
+ }
+
+ /**
+ * Paint redraws the whole screen.
+ *
+ * @param gr the Swing Graphics context
+ */
+ @Override
+ public void paint(final Graphics gr) {
+ if (screen != null) {
+ screen.paint(gr);
+ }
+ }
+ };
+
+ // Set icon
+ ClassLoader loader = Thread.currentThread().
+ getContextClassLoader();
+ frame.setIconImage((new ImageIcon(loader.
+ getResource(ICONFILE))).getImage());
+
+ // Get the Swing component
+ SwingTerminal.this.swing = new SwingComponent(frame);
+
+ // Hang onto top and left for drawing.
+ Insets insets = SwingTerminal.this.swing.getInsets();
+ SwingTerminal.this.left = insets.left;
+ SwingTerminal.this.top = insets.top;
+
+ // Load the font so that we can set sessionInfo.
+ setDefaultFont();
+
+ // Get the default cols x rows and set component size
+ // accordingly.
+ SwingTerminal.this.sessionInfo =
+ new SwingSessionInfo(SwingTerminal.this.swing,
+ SwingTerminal.this.textWidth,
+ SwingTerminal.this.textHeight,
+ windowWidth, windowHeight);
+
+ SwingTerminal.this.setDimensions(sessionInfo.
+ getWindowWidth(), sessionInfo.getWindowHeight());
+
+ SwingTerminal.this.resizeToScreen(true);
+ SwingTerminal.this.swing.setVisible(true);
+ }
+ });
+ } catch (java.lang.reflect.InvocationTargetException e) {
+ e.printStackTrace();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ this.listener = listener;
+ mouse1 = false;
+ mouse2 = false;
+ mouse3 = false;
+ eventQueue = new ArrayList<TInputEvent>();
+
+ // Add listeners to Swing.
+ swing.addKeyListener(this);
+ swing.addWindowListener(this);
+ swing.addComponentListener(this);
+ swing.addMouseListener(this);
+ swing.addMouseMotionListener(this);
+ swing.addMouseWheelListener(this);
+ }
+
+ /**
+ * Public constructor renders to an existing JComponent.
+ *
+ * @param component the Swing component to render to
+ * @param windowWidth the number of text columns to start with
+ * @param windowHeight the number of text rows to start with
+ * @param fontSize the size in points. Good values to pick are: 16, 20,
+ * 22, and 24.
+ * @param listener the object this backend needs to wake up when new
+ * input comes in
+ */
+ public SwingTerminal(final JComponent component, final int windowWidth,
+ final int windowHeight, final int fontSize, final Object listener) {
+
+ this.fontSize = fontSize;
+
+ reloadOptions();
+
+ try {
+ SwingUtilities.invokeAndWait(new Runnable() {
+ public void run() {
+
+ JComponent newComponent = new JComponent() {
+
+ /**
+ * Serializable version.
+ */
+ private static final long serialVersionUID = 1;
+
+ /**
+ * The code that performs the actual drawing.
+ */
+ public SwingTerminal screen = null;
+
+ /*
+ * Anonymous class initializer saves the screen
+ * reference, so that paint() and the like call out
+ * to SwingTerminal.
+ */
+ {
+ this.screen = SwingTerminal.this;
+ }
+
+ /**
+ * Update redraws the whole screen.
+ *
+ * @param gr the Swing Graphics context
+ */
+ @Override
+ public void update(final Graphics gr) {
+ // The default update clears the area. Don't do
+ // that, instead just paint it directly.
+ paint(gr);
+ }
+
+ /**
+ * Paint redraws the whole screen.
+ *
+ * @param gr the Swing Graphics context
+ */
+ @Override
+ public void paint(final Graphics gr) {
+ if (screen != null) {
+ screen.paint(gr);
+ }
+ }
+ };
+ component.setLayout(new BorderLayout());
+ component.add(newComponent);
+
+ // Allow key events to be received
+ component.setFocusable(true);
+
+ // Get the Swing component
+ SwingTerminal.this.swing = new SwingComponent(component);
+
+ // Hang onto top and left for drawing.
+ Insets insets = SwingTerminal.this.swing.getInsets();
+ SwingTerminal.this.left = insets.left;
+ SwingTerminal.this.top = insets.top;
+
+ // Load the font so that we can set sessionInfo.
+ setDefaultFont();
+
+ // Get the default cols x rows and set component size
+ // accordingly.
+ SwingTerminal.this.sessionInfo =
+ new SwingSessionInfo(SwingTerminal.this.swing,
+ SwingTerminal.this.textWidth,
+ SwingTerminal.this.textHeight);
+ }
+ });
+ } catch (java.lang.reflect.InvocationTargetException e) {
+ e.printStackTrace();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ this.listener = listener;
+ mouse1 = false;
+ mouse2 = false;
+ mouse3 = false;
+ eventQueue = new ArrayList<TInputEvent>();
+
+ // Add listeners to Swing.
+ swing.addKeyListener(this);
+ swing.addWindowListener(this);
+ swing.addComponentListener(this);
+ swing.addMouseListener(this);
+ swing.addMouseMotionListener(this);
+ swing.addMouseWheelListener(this);
+ }
+
+ // ------------------------------------------------------------------------
+ // LogicalScreen ----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Set the window title.
+ *
+ * @param title the new title
+ */
+ @Override
+ public void setTitle(final String title) {
+ swing.setTitle(title);
+ }
+
+ /**
+ * Push the logical screen to the physical device.
+ */
+ @Override
+ public void flushPhysical() {
+ // See if it is time to flip the blink time.
+ long nowTime = System.currentTimeMillis();
+ if (nowTime >= blinkMillis + lastBlinkTime) {
+ lastBlinkTime = nowTime;
+ cursorBlinkVisible = !cursorBlinkVisible;
+ // System.err.println("New lastBlinkTime: " + lastBlinkTime);
+ }
+
+ if ((swing.getFrame() != null)
+ && (swing.getBufferStrategy() != null)
+ ) {
+ 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();
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // TerminalReader ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Check if there are events in the queue.
+ *
+ * @return if true, getEvents() has something to return to the backend
+ */
+ public boolean hasEvents() {
+ synchronized (eventQueue) {
+ return (eventQueue.size() > 0);
+ }
+ }
+
+ /**
+ * Return any events in the IO queue.
+ *
+ * @param queue list to append new events to
+ */
+ public void getEvents(final List<TInputEvent> queue) {
+ synchronized (eventQueue) {
+ if (eventQueue.size() > 0) {
+ synchronized (queue) {
+ queue.addAll(eventQueue);
+ }
+ eventQueue.clear();
+ }
+ }
+ }
+
+ /**
+ * Restore terminal to normal state.
+ */
+ public void closeTerminal() {
+ shutdown();
+ }
+
+ /**
+ * 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) {
+ this.listener = listener;
+ }
+
+ /**
+ * Reload options from System properties.
+ */
+ public void reloadOptions() {
+ // Figure out my cursor style.
+ String cursorStyleString = System.getProperty(
+ "jexer.Swing.cursorStyle", "underline").toLowerCase();
+ if (cursorStyleString.equals("underline")) {
+ cursorStyle = CursorStyle.UNDERLINE;
+ } else if (cursorStyleString.equals("outline")) {
+ cursorStyle = CursorStyle.OUTLINE;
+ } else if (cursorStyleString.equals("block")) {
+ cursorStyle = CursorStyle.BLOCK;
+ } else if (cursorStyleString.equals("verticalbar")) {
+ cursorStyle = CursorStyle.VERTICAL_BAR;
+ }
+
+ // Pull the system property for triple buffering.
+ if (System.getProperty("jexer.Swing.tripleBuffer",
+ "true").equals("true")
+ ) {
+ SwingComponent.tripleBuffer = true;
+ } else {
+ SwingComponent.tripleBuffer = false;
+ }
+
+ // Set custom colors
+ setCustomSystemColors();
+ }
+
+ // ------------------------------------------------------------------------
+ // SwingTerminal ----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get the width of a character cell in pixels.
+ *
+ * @return the width in pixels of a character cell
+ */
+ public int getTextWidth() {
+ return textWidth;
+ }
+
+ /**
+ * Get the height of a character cell in pixels.
+ *
+ * @return the height in pixels of a character cell
+ */
+ public int getTextHeight() {
+ return textHeight;
+ }
+
+ /**
+ * Setup Swing colors to match DOS color palette.
+ */
+ private static void setDOSColors() {
+ if (dosColors) {
+ return;
+ }
+ MYBLACK = new Color(0x00, 0x00, 0x00);
+ MYRED = new Color(0xa8, 0x00, 0x00);
+ MYGREEN = new Color(0x00, 0xa8, 0x00);
+ MYYELLOW = new Color(0xa8, 0x54, 0x00);
+ MYBLUE = new Color(0x00, 0x00, 0xa8);
+ MYMAGENTA = new Color(0xa8, 0x00, 0xa8);
+ MYCYAN = new Color(0x00, 0xa8, 0xa8);
+ MYWHITE = new Color(0xa8, 0xa8, 0xa8);
+ MYBOLD_BLACK = new Color(0x54, 0x54, 0x54);
+ MYBOLD_RED = new Color(0xfc, 0x54, 0x54);
+ MYBOLD_GREEN = new Color(0x54, 0xfc, 0x54);
+ MYBOLD_YELLOW = new Color(0xfc, 0xfc, 0x54);
+ MYBOLD_BLUE = new Color(0x54, 0x54, 0xfc);
+ MYBOLD_MAGENTA = new Color(0xfc, 0x54, 0xfc);
+ MYBOLD_CYAN = new Color(0x54, 0xfc, 0xfc);
+ MYBOLD_WHITE = new Color(0xfc, 0xfc, 0xfc);
+
+ dosColors = true;
+ }
+
+ /**
+ * Setup Swing colors to match those provided in system properties.
+ */
+ private static void setCustomSystemColors() {
+ synchronized (SwingTerminal.class) {
+ MYBLACK = getCustomColor("jexer.Swing.color0", MYBLACK);
+ MYRED = getCustomColor("jexer.Swing.color1", MYRED);
+ MYGREEN = getCustomColor("jexer.Swing.color2", MYGREEN);
+ MYYELLOW = getCustomColor("jexer.Swing.color3", MYYELLOW);
+ MYBLUE = getCustomColor("jexer.Swing.color4", MYBLUE);
+ MYMAGENTA = getCustomColor("jexer.Swing.color5", MYMAGENTA);
+ MYCYAN = getCustomColor("jexer.Swing.color6", MYCYAN);
+ MYWHITE = getCustomColor("jexer.Swing.color7", MYWHITE);
+ MYBOLD_BLACK = getCustomColor("jexer.Swing.color8", MYBOLD_BLACK);
+ MYBOLD_RED = getCustomColor("jexer.Swing.color9", MYBOLD_RED);
+ MYBOLD_GREEN = getCustomColor("jexer.Swing.color10", MYBOLD_GREEN);
+ MYBOLD_YELLOW = getCustomColor("jexer.Swing.color11", MYBOLD_YELLOW);
+ MYBOLD_BLUE = getCustomColor("jexer.Swing.color12", MYBOLD_BLUE);
+ MYBOLD_MAGENTA = getCustomColor("jexer.Swing.color13", MYBOLD_MAGENTA);
+ MYBOLD_CYAN = getCustomColor("jexer.Swing.color14", MYBOLD_CYAN);
+ MYBOLD_WHITE = getCustomColor("jexer.Swing.color15", MYBOLD_WHITE);
+ }
+ }
+
+ /**
+ * Setup one Swing color to match the RGB value provided in system
+ * properties.
+ *
+ * @param key the system property key
+ * @param defaultColor the default color to return if key is not set, or
+ * incorrect
+ * @return a color from the RGB string, or defaultColor
+ */
+ private static Color getCustomColor(final String key,
+ final Color defaultColor) {
+
+ String rgb = System.getProperty(key);
+ if (rgb == null) {
+ return defaultColor;
+ }
+ if (rgb.startsWith("#")) {
+ rgb = rgb.substring(1);
+ }
+ int rgbInt = 0;
+ try {
+ rgbInt = Integer.parseInt(rgb, 16);
+ } catch (NumberFormatException e) {
+ return defaultColor;
+ }
+ Color color = new Color((rgbInt & 0xFF0000) >>> 16,
+ (rgbInt & 0x00FF00) >>> 8,
+ (rgbInt & 0x0000FF));
+
+ return color;
+ }
+
+ /**
+ * Get the number of millis to wait before switching the blink from
+ * visible to invisible.
+ *
+ * @return the number of milli to wait before switching the blink from
+ * visible to invisible
+ */
+ public long getBlinkMillis() {
+ return blinkMillis;
+ }
+
+ /**
+ * Get the current status of the blink flag.
+ *
+ * @return true if the cursor and blinking text should be visible
+ */
+ public boolean getCursorBlinkVisible() {
+ return cursorBlinkVisible;
+ }
+
+ /**
+ * Get the font size in points.
+ *
+ * @return font size in points
+ */
+ public int getFontSize() {
+ return fontSize;
+ }
+
+ /**
+ * Set the font size in points.
+ *
+ * @param fontSize font size in points
+ */
+ public void setFontSize(final int fontSize) {
+ this.fontSize = fontSize;
+ Font newFont = font.deriveFont((float) fontSize);
+ setFont(newFont);
+ }
+
+ /**
+ * Set to a new font, and resize the screen to match its dimensions.
+ *
+ * @param font the new font
+ */
+ public void setFont(final Font font) {
+ if (!SwingUtilities.isEventDispatchThread()) {
+ // Not in the Swing thread: force this inside the Swing thread.
+ try {
+ SwingUtilities.invokeAndWait(new Runnable() {
+ public void run() {
+ synchronized (this) {
+ SwingTerminal.this.font = font;
+ getFontDimensions();
+ swing.setFont(font);
+ glyphCacheBlink = new HashMap<Cell, BufferedImage>();
+ glyphCache = new HashMap<Cell, BufferedImage>();
+ resizeToScreen(true);
+ }
+ }
+ });
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } catch (java.lang.reflect.InvocationTargetException e) {
+ e.printStackTrace();
+ }
+ } else {
+ synchronized (this) {
+ SwingTerminal.this.font = font;
+ getFontDimensions();
+ swing.setFont(font);
+ glyphCacheBlink = new HashMap<Cell, BufferedImage>();
+ glyphCache = new HashMap<Cell, BufferedImage>();
+ resizeToScreen(true);
+ }
+ }
+ }
+
+ /**
+ * Get the font this screen was last set to.
+ *
+ * @return the font
+ */
+ public Font getFont() {
+ return font;
+ }
+
+ /**
+ * Set the font to Terminus, the best all-around font for both CP437 and
+ * ISO8859-1.
+ */
+ public void setDefaultFont() {
+ try {
+ ClassLoader loader = Thread.currentThread().getContextClassLoader();
+ InputStream in = loader.getResourceAsStream(FONTFILE);
+ Font terminusRoot = Font.createFont(Font.TRUETYPE_FONT, in);
+ Font terminus = terminusRoot.deriveFont(Font.PLAIN, fontSize);
+ font = terminus;
+ } catch (java.awt.FontFormatException e) {
+ e.printStackTrace();
+ font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize);
+ } catch (java.io.IOException e) {
+ e.printStackTrace();
+ font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize);
+ }
+
+ setFont(font);
+ }
+
+ /**
+ * Get the X text adjustment.
+ *
+ * @return X text adjustment
+ */
+ public int getTextAdjustX() {
+ return textAdjustX;
+ }
+
+ /**
+ * Set the X text adjustment.
+ *
+ * @param textAdjustX the X text adjustment
+ */
+ public void setTextAdjustX(final int textAdjustX) {
+ synchronized (this) {
+ this.textAdjustX = textAdjustX;
+ glyphCacheBlink = new HashMap<Cell, BufferedImage>();
+ glyphCache = new HashMap<Cell, BufferedImage>();
+ clearPhysical();
+ }
+ }
+
+ /**
+ * Get the Y text adjustment.
+ *
+ * @return Y text adjustment
+ */
+ public int getTextAdjustY() {
+ return textAdjustY;
+ }
+
+ /**
+ * Set the Y text adjustment.
+ *
+ * @param textAdjustY the Y text adjustment
+ */
+ public void setTextAdjustY(final int textAdjustY) {
+ synchronized (this) {
+ this.textAdjustY = textAdjustY;
+ glyphCacheBlink = new HashMap<Cell, BufferedImage>();
+ glyphCache = new HashMap<Cell, BufferedImage>();
+ clearPhysical();
+ }
+ }
+
+ /**
+ * Get the height text adjustment.
+ *
+ * @return height text adjustment
+ */
+ public int getTextAdjustHeight() {
+ return textAdjustHeight;
+ }
+
+ /**
+ * Set the height text adjustment.
+ *
+ * @param textAdjustHeight the height text adjustment
+ */
+ public void setTextAdjustHeight(final int textAdjustHeight) {
+ synchronized (this) {
+ this.textAdjustHeight = textAdjustHeight;
+ textHeight = fontTextHeight + textAdjustHeight;
+ glyphCacheBlink = new HashMap<Cell, BufferedImage>();
+ glyphCache = new HashMap<Cell, BufferedImage>();
+ clearPhysical();
+ }
+ }
+
+ /**
+ * Get the width text adjustment.
+ *
+ * @return width text adjustment
+ */
+ public int getTextAdjustWidth() {
+ return textAdjustWidth;
+ }
+
+ /**
+ * Set the width text adjustment.
+ *
+ * @param textAdjustWidth the width text adjustment
+ */
+ public void setTextAdjustWidth(final int textAdjustWidth) {
+ synchronized (this) {
+ this.textAdjustWidth = textAdjustWidth;
+ textWidth = fontTextWidth + textAdjustWidth;
+ glyphCacheBlink = new HashMap<Cell, BufferedImage>();
+ glyphCache = new HashMap<Cell, BufferedImage>();
+ clearPhysical();
+ }
+ }
+
+ /**
+ * Convert a CellAttributes foreground color to an Swing Color.
+ *
+ * @param attr the text attributes
+ * @return the Swing Color
+ */
+ public static Color attrToForegroundColor(final CellAttributes attr) {
+ int rgb = attr.getForeColorRGB();
+ if (rgb >= 0) {
+ int red = (rgb >> 16) & 0xFF;
+ int green = (rgb >> 8) & 0xFF;
+ int blue = rgb & 0xFF;
+
+ return new Color(red, green, blue);
+ }
+
+ if (attr.isBold()) {
+ if (attr.getForeColor().equals(jexer.bits.Color.BLACK)) {
+ return MYBOLD_BLACK;
+ } else if (attr.getForeColor().equals(jexer.bits.Color.RED)) {
+ return MYBOLD_RED;
+ } else if (attr.getForeColor().equals(jexer.bits.Color.BLUE)) {
+ return MYBOLD_BLUE;
+ } else if (attr.getForeColor().equals(jexer.bits.Color.GREEN)) {
+ return MYBOLD_GREEN;
+ } else if (attr.getForeColor().equals(jexer.bits.Color.YELLOW)) {
+ return MYBOLD_YELLOW;
+ } else if (attr.getForeColor().equals(jexer.bits.Color.CYAN)) {
+ return MYBOLD_CYAN;
+ } else if (attr.getForeColor().equals(jexer.bits.Color.MAGENTA)) {
+ return MYBOLD_MAGENTA;
+ } else if (attr.getForeColor().equals(jexer.bits.Color.WHITE)) {
+ return MYBOLD_WHITE;
+ }
+ } else {
+ if (attr.getForeColor().equals(jexer.bits.Color.BLACK)) {
+ return MYBLACK;
+ } else if (attr.getForeColor().equals(jexer.bits.Color.RED)) {
+ return MYRED;
+ } else if (attr.getForeColor().equals(jexer.bits.Color.BLUE)) {
+ return MYBLUE;
+ } else if (attr.getForeColor().equals(jexer.bits.Color.GREEN)) {
+ return MYGREEN;
+ } else if (attr.getForeColor().equals(jexer.bits.Color.YELLOW)) {
+ return MYYELLOW;
+ } else if (attr.getForeColor().equals(jexer.bits.Color.CYAN)) {
+ return MYCYAN;
+ } else if (attr.getForeColor().equals(jexer.bits.Color.MAGENTA)) {
+ return MYMAGENTA;
+ } else if (attr.getForeColor().equals(jexer.bits.Color.WHITE)) {
+ return MYWHITE;
+ }
+ }
+ throw new IllegalArgumentException("Invalid color: " +
+ attr.getForeColor().getValue());
+ }
+
+ /**
+ * Convert a CellAttributes background color to an Swing Color.
+ *
+ * @param attr the text attributes
+ * @return the Swing Color
+ */
+ public static Color attrToBackgroundColor(final CellAttributes attr) {
+ int rgb = attr.getBackColorRGB();
+ if (rgb >= 0) {
+ int red = (rgb >> 16) & 0xFF;
+ int green = (rgb >> 8) & 0xFF;
+ int blue = rgb & 0xFF;
+
+ return new Color(red, green, blue);
+ }
+
+ if (attr.getBackColor().equals(jexer.bits.Color.BLACK)) {
+ return MYBLACK;
+ } else if (attr.getBackColor().equals(jexer.bits.Color.RED)) {
+ return MYRED;
+ } else if (attr.getBackColor().equals(jexer.bits.Color.BLUE)) {
+ return MYBLUE;
+ } else if (attr.getBackColor().equals(jexer.bits.Color.GREEN)) {
+ return MYGREEN;
+ } else if (attr.getBackColor().equals(jexer.bits.Color.YELLOW)) {
+ return MYYELLOW;
+ } else if (attr.getBackColor().equals(jexer.bits.Color.CYAN)) {
+ return MYCYAN;
+ } else if (attr.getBackColor().equals(jexer.bits.Color.MAGENTA)) {
+ return MYMAGENTA;
+ } else if (attr.getBackColor().equals(jexer.bits.Color.WHITE)) {
+ return MYWHITE;
+ }
+ throw new IllegalArgumentException("Invalid color: " +
+ attr.getBackColor().getValue());
+ }
+
+ /**
+ * Figure out what textAdjustX, textAdjustY, textAdjustHeight, and
+ * textAdjustWidth should be, based on the location of a vertical bar and
+ * a horizontal bar.
+ */
+ private void getFontAdjustments() {
+ BufferedImage image = null;
+
+ // What SHOULD happen is that the topmost/leftmost white pixel is at
+ // position (gr2x, gr2y). But it might also be off by a pixel in
+ // either direction.
+
+ Graphics2D gr2 = null;
+ int gr2x = 3;
+ int gr2y = 3;
+ image = new BufferedImage(fontTextWidth * 2, fontTextHeight * 2,
+ BufferedImage.TYPE_INT_ARGB);
+
+ gr2 = image.createGraphics();
+ gr2.setFont(swing.getFont());
+ gr2.setColor(java.awt.Color.BLACK);
+ gr2.fillRect(0, 0, fontTextWidth * 2, fontTextHeight * 2);
+ gr2.setColor(java.awt.Color.WHITE);
+ char [] chars = new char[1];
+ chars[0] = jexer.bits.GraphicsChars.SINGLE_BAR;
+ gr2.drawChars(chars, 0, 1, gr2x, gr2y + fontTextHeight - maxDescent);
+ chars[0] = jexer.bits.GraphicsChars.VERTICAL_BAR;
+ gr2.drawChars(chars, 0, 1, gr2x, gr2y + fontTextHeight - maxDescent);
+ gr2.dispose();
+
+ int top = fontTextHeight * 2;
+ int bottom = -1;
+ int left = fontTextWidth * 2;
+ int right = -1;
+ textAdjustX = 0;
+ textAdjustY = 0;
+ textAdjustHeight = 0;
+ textAdjustWidth = 0;
+
+ for (int x = 0; x < fontTextWidth * 2; x++) {
+ for (int y = 0; y < fontTextHeight * 2; y++) {
+
+ /*
+ System.err.println("H X: " + x + " Y: " + y + " " +
+ image.getRGB(x, y));
+ */
+
+ if ((image.getRGB(x, y) & 0xFFFFFF) != 0) {
+ // Pixel is present.
+ if (y < top) {
+ top = y;
+ }
+ if (y > bottom) {
+ bottom = y;
+ }
+ if (x < left) {
+ left = x;
+ }
+ if (x > right) {
+ right = x;
+ }
+ }
+ }
+ }
+ if (left < right) {
+ textAdjustX = (gr2x - left);
+ textAdjustWidth = fontTextWidth - (right - left + 1);
+ }
+ if (top < bottom) {
+ textAdjustY = (gr2y - top);
+ textAdjustHeight = fontTextHeight - (bottom - top + 1);
+ }
+ // System.err.println("top " + top + " bottom " + bottom);
+ // System.err.println("left " + left + " right " + right);
+
+ // Special case: do not believe fonts that claim to be wider than
+ // they are tall.
+ if (fontTextWidth >= fontTextHeight) {
+ textAdjustX = 0;
+ textAdjustWidth = 0;
+ fontTextWidth = fontTextHeight / 2;
+ }
+ }
+
+ /**
+ * Figure out my font dimensions. This code path works OK for the JFrame
+ * case, and can be called immediately after JFrame creation.
+ */
+ private void getFontDimensions() {
+ swing.setFont(font);
+ Graphics gr = swing.getGraphics();
+ if (gr == null) {
+ return;
+ }
+ getFontDimensions(gr);
+ }
+
+ /**
+ * Figure out my font dimensions. This code path is needed to lazy-load
+ * the information inside paint().
+ *
+ * @param gr Graphics object to use
+ */
+ private void getFontDimensions(final Graphics gr) {
+ swing.setFont(font);
+ FontMetrics fm = gr.getFontMetrics();
+ maxDescent = fm.getMaxDescent();
+ Rectangle2D bounds = fm.getMaxCharBounds(gr);
+ int leading = fm.getLeading();
+ fontTextWidth = (int)Math.round(bounds.getWidth());
+ // fontTextHeight = (int)Math.round(bounds.getHeight()) - maxDescent;
+
+ // This produces the same number, but works better for ugly
+ // monospace.
+ fontTextHeight = fm.getMaxAscent() + maxDescent - leading;
+
+ getFontAdjustments();
+ textHeight = fontTextHeight + textAdjustHeight;
+ textWidth = fontTextWidth + textAdjustWidth;
+
+ if (sessionInfo != null) {
+ sessionInfo.setTextCellDimensions(textWidth, textHeight);
+ }
+ gotFontDimensions = true;
+ }
+
+ /**
+ * Resize the physical screen to match the logical screen dimensions.
+ *
+ * @param resizeComponent if true, resize the Swing component
+ */
+ private void resizeToScreen(final boolean resizeComponent) {
+ if (resizeComponent) {
+ swing.setDimensions(textWidth * width, textHeight * height);
+ }
+ clearPhysical();
+ }
+
+ /**
+ * Resize the physical screen to match the logical screen dimensions.
+ */
+ @Override
+ public void resizeToScreen() {
+ resizeToScreen(false);
+ }
+
+ /**
+ * Draw one cell's image to the screen.
+ *
+ * @param gr the Swing Graphics context
+ * @param cell the Cell to draw
+ * @param xPixel the x-coordinate to render to. 0 means the
+ * left-most pixel column.
+ * @param yPixel the y-coordinate to render to. 0 means the top-most
+ * pixel row.
+ */
+ private void drawImage(final Graphics gr, final Cell cell,
+ final int xPixel, final int yPixel) {
+
+ /*
+ System.err.println("drawImage(): " + xPixel + " " + yPixel +
+ " " + cell);
+ */
+
+ // Draw the background rectangle, then the foreground character.
+ assert (cell.isImage());
+ 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());
+ } else {
+ gr.drawImage(image, xPixel, yPixel, swing.getComponent());
+ }
+ return;
+ }
+ }
+
+ /**
+ * Draw one glyph to the screen.
+ *
+ * @param gr the Swing Graphics context
+ * @param cell the Cell to draw
+ * @param xPixel the x-coordinate to render to. 0 means the
+ * left-most pixel column.
+ * @param yPixel the y-coordinate to render to. 0 means the top-most
+ * pixel row.
+ */
+ private void drawGlyph(final Graphics gr, final Cell cell,
+ final int xPixel, final int yPixel) {
+
+ /*
+ System.err.println("drawGlyph(): " + xPixel + " " + yPixel +
+ " " + cell);
+ */
+
+ BufferedImage image = null;
+ if (cell.isBlink() && !cursorBlinkVisible) {
+ image = glyphCacheBlink.get(cell);
+ } else {
+ image = glyphCache.get(cell);
+ }
+ if (image != null) {
+ if (swing.getFrame() != null) {
+ gr.drawImage(image, xPixel, yPixel, swing.getFrame());
+ } else {
+ gr.drawImage(image, xPixel, yPixel, swing.getComponent());
+ }
+ return;
+ }
+
+ // Generate glyph and draw it.
+ Graphics2D gr2 = null;
+ int gr2x = xPixel;
+ int gr2y = yPixel;
+ if ((SwingComponent.tripleBuffer) && (swing.getFrame() != null)) {
+ image = new BufferedImage(textWidth, textHeight,
+ BufferedImage.TYPE_INT_ARGB);
+ gr2 = image.createGraphics();
+ gr2.setFont(swing.getFont());
+ gr2x = 0;
+ gr2y = 0;
+ } else {
+ gr2 = (Graphics2D) gr;
+ }
+
+ Cell cellColor = new Cell(cell);
+
+ // Check for reverse
+ if (cell.isReverse()) {
+ cellColor.setForeColor(cell.getBackColor());
+ cellColor.setBackColor(cell.getForeColor());
+ }
+
+ // Draw the background rectangle, then the foreground character.
+ gr2.setColor(attrToBackgroundColor(cellColor));
+ gr2.fillRect(gr2x, gr2y, textWidth, textHeight);
+
+ // Handle blink and underline
+ if (!cell.isBlink()
+ || (cell.isBlink() && cursorBlinkVisible)
+ ) {
+ gr2.setColor(attrToForegroundColor(cellColor));
+ char [] chars = Character.toChars(cell.getChar());
+ gr2.drawChars(chars, 0, chars.length, gr2x + textAdjustX,
+ gr2y + textHeight - maxDescent + textAdjustY);
+
+ if (cell.isUnderline()) {
+ gr2.fillRect(gr2x, gr2y + textHeight - 2, textWidth, 2);
+ }
+ }
+
+ if ((SwingComponent.tripleBuffer) && (swing.getFrame() != null)) {
+ gr2.dispose();
+
+ // We need a new key that will not be mutated by
+ // invertCell().
+ Cell key = new Cell(cell);
+ if (cell.isBlink() && !cursorBlinkVisible) {
+ glyphCacheBlink.put(key, image);
+ } else {
+ glyphCache.put(key, image);
+ }
+
+ if (swing.getFrame() != null) {
+ gr.drawImage(image, xPixel, yPixel, swing.getFrame());
+ } else {
+ gr.drawImage(image, xPixel, yPixel, swing.getComponent());
+ }
+ }
+
+ }
+
+ /**
+ * Check if the cursor is visible, and if so draw it.
+ *
+ * @param gr the Swing Graphics context
+ */
+ private void drawCursor(final Graphics gr) {
+
+ if (cursorVisible
+ && (cursorY >= 0)
+ && (cursorX >= 0)
+ && (cursorY <= height - 1)
+ && (cursorX <= width - 1)
+ && cursorBlinkVisible
+ ) {
+ int xPixel = cursorX * textWidth + left;
+ int yPixel = cursorY * textHeight + top;
+ Cell lCell = logical[cursorX][cursorY];
+ int cursorWidth = textWidth;
+ switch (lCell.getWidth()) {
+ case SINGLE:
+ // NOP
+ break;
+ case LEFT:
+ cursorWidth *= 2;
+ break;
+ case RIGHT:
+ cursorWidth *= 2;
+ xPixel -= textWidth;
+ break;
+ }
+ gr.setColor(attrToForegroundColor(lCell));
+ switch (cursorStyle) {
+ default:
+ // Fall through...
+ case UNDERLINE:
+ gr.fillRect(xPixel, yPixel + textHeight - 2, cursorWidth, 2);
+ break;
+ case BLOCK:
+ gr.fillRect(xPixel, yPixel, cursorWidth, textHeight);
+ break;
+ case OUTLINE:
+ gr.drawRect(xPixel, yPixel, cursorWidth - 1, textHeight - 1);
+ break;
+ case VERTICAL_BAR:
+ gr.fillRect(xPixel, yPixel, 2, textHeight);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Reset the blink timer.
+ */
+ private void resetBlinkTimer() {
+ lastBlinkTime = System.currentTimeMillis();
+ cursorBlinkVisible = true;
+ }
+
+ /**
+ * Paint redraws the whole screen.
+ *
+ * @param gr the Swing Graphics context
+ */
+ public void paint(final Graphics gr) {
+
+ if (gotFontDimensions == false) {
+ // Lazy-load the text width/height
+ getFontDimensions(gr);
+ /*
+ System.err.println("textWidth " + textWidth +
+ " textHeight " + textHeight);
+ System.err.println("FONT: " + swing.getFont() + " font " + font);
+ */
+ }
+
+ if ((swing.getFrame() != null)
+ && (swing.getBufferStrategy() != null)
+ && (SwingUtilities.isEventDispatchThread())
+ ) {
+ // System.err.println("paint(), skip first paint on swing thread");
+ return;
+ }
+
+ int xCellMin = 0;
+ int xCellMax = width;
+ int yCellMin = 0;
+ int yCellMax = height;
+
+ Rectangle bounds = gr.getClipBounds();
+ if (bounds != null) {
+ // Only update what is in the bounds
+ xCellMin = textColumn(bounds.x);
+ xCellMax = textColumn(bounds.x + bounds.width) + 1;
+ if (xCellMax > width) {
+ xCellMax = width;
+ }
+ if (xCellMin >= xCellMax) {
+ xCellMin = xCellMax - 2;
+ }
+ if (xCellMin < 0) {
+ xCellMin = 0;
+ }
+ yCellMin = textRow(bounds.y);
+ yCellMax = textRow(bounds.y + bounds.height) + 1;
+ if (yCellMax > height) {
+ yCellMax = height;
+ }
+ if (yCellMin >= yCellMax) {
+ yCellMin = yCellMax - 2;
+ }
+ if (yCellMin < 0) {
+ yCellMin = 0;
+ }
+ } else {
+ // We need a total repaint
+ reallyCleared = true;
+ }
+
+ // Prevent updates to the screen's data from the TApplication
+ // threads.
+ synchronized (this) {
+
+ /*
+ System.err.printf("bounds %s X %d %d Y %d %d\n",
+ bounds, xCellMin, xCellMax, yCellMin, yCellMax);
+ */
+
+ for (int y = yCellMin; y < yCellMax; y++) {
+ for (int x = xCellMin; x < xCellMax; x++) {
+
+ int xPixel = x * textWidth + left;
+ int yPixel = y * textHeight + top;
+
+ Cell lCell = logical[x][y];
+ Cell pCell = physical[x][y];
+
+ if (!lCell.equals(pCell)
+ || lCell.isBlink()
+ || reallyCleared
+ || (swing.getFrame() == null)) {
+
+ if (lCell.isImage()) {
+ drawImage(gr, lCell, xPixel, yPixel);
+ } else {
+ drawGlyph(gr, lCell, xPixel, yPixel);
+ }
+
+ // Physical is always updated
+ physical[x][y].setTo(lCell);
+ }
+ }
+ }
+ drawCursor(gr);
+
+ reallyCleared = false;
+ } // synchronized (this)
+ }
+
+ /**
+ * Restore terminal to normal state.
+ */
+ public void shutdown() {
+ swing.dispose();
+ }
+
+ /**
+ * Push the logical screen to the physical device.
+ */
+ private void drawToSwing() {
+
+ /*
+ System.err.printf("drawToSwing(): reallyCleared %s dirty %s\n",
+ reallyCleared, dirty);
+ */
+
+ // If reallyCleared is set, we have to draw everything.
+ if ((swing.getFrame() != null)
+ && (swing.getBufferStrategy() != null)
+ && (reallyCleared == true)
+ ) {
+ // Triple-buffering: we have to redraw everything on this thread.
+ Graphics gr = swing.getBufferStrategy().getDrawGraphics();
+ swing.paint(gr);
+ gr.dispose();
+ swing.getBufferStrategy().show();
+ Toolkit.getDefaultToolkit().sync();
+ return;
+ } else if (((swing.getFrame() != null)
+ && (swing.getBufferStrategy() == null))
+ || (reallyCleared == true)
+ ) {
+ // Repaint everything on the Swing thread.
+ // System.err.println("REPAINT ALL");
+ swing.repaint();
+ return;
+ }
+
+ if ((swing.getFrame() != null) && (swing.getBufferStrategy() != null)) {
+ Graphics gr = swing.getBufferStrategy().getDrawGraphics();
+
+ synchronized (this) {
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ Cell lCell = logical[x][y];
+ Cell pCell = physical[x][y];
+
+ int xPixel = x * textWidth + left;
+ int yPixel = y * textHeight + top;
+
+ if (!lCell.equals(pCell)
+ || ((x == cursorX)
+ && (y == cursorY)
+ && cursorVisible)
+ || (lCell.isBlink())
+ ) {
+ if (lCell.isImage()) {
+ drawImage(gr, lCell, xPixel, yPixel);
+ } else {
+ drawGlyph(gr, lCell, xPixel, yPixel);
+ }
+ physical[x][y].setTo(lCell);
+ }
+ }
+ }
+ drawCursor(gr);
+ } // synchronized (this)
+
+ gr.dispose();
+ swing.getBufferStrategy().show();
+ Toolkit.getDefaultToolkit().sync();
+ return;
+ }
+
+ // Swing thread version: request a repaint, but limit it to the area
+ // that has changed.
+
+ // Find the minimum-size damaged region.
+ int xMin = swing.getWidth();
+ int xMax = 0;
+ int yMin = swing.getHeight();
+ int yMax = 0;
+
+ synchronized (this) {
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ Cell lCell = logical[x][y];
+ Cell pCell = physical[x][y];
+
+ int xPixel = x * textWidth + left;
+ int yPixel = y * textHeight + top;
+
+ if (!lCell.equals(pCell)
+ || ((x == cursorX)
+ && (y == cursorY)
+ && cursorVisible)
+ || lCell.isBlink()
+ ) {
+ if (xPixel < xMin) {
+ xMin = xPixel;
+ }
+ if (xPixel + textWidth > xMax) {
+ xMax = xPixel + textWidth;
+ }
+ if (yPixel < yMin) {
+ yMin = yPixel;
+ }
+ if (yPixel + textHeight > yMax) {
+ yMax = yPixel + textHeight;
+ }
+ }
+ }
+ }
+ }
+ if (xMin + textWidth >= xMax) {
+ xMax += textWidth;
+ }
+ if (yMin + textHeight >= yMax) {
+ yMax += textHeight;
+ }
+
+ // Repaint the desired area
+ /*
+ System.err.printf("REPAINT X %d %d Y %d %d\n", xMin, xMax,
+ yMin, yMax);
+ */
+
+ if ((swing.getFrame() != null) && (swing.getBufferStrategy() != null)) {
+ // This path should never be taken, but is left here for
+ // completeness.
+ Graphics gr = swing.getBufferStrategy().getDrawGraphics();
+ Rectangle bounds = new Rectangle(xMin, yMin, xMax - xMin,
+ yMax - yMin);
+ gr.setClip(bounds);
+ swing.paint(gr);
+ gr.dispose();
+ swing.getBufferStrategy().show();
+ Toolkit.getDefaultToolkit().sync();
+ } else {
+ // Repaint on the Swing thread.
+ swing.repaint(xMin, yMin, xMax - xMin, yMax - yMin);
+ }
+ }
+
+ /**
+ * Convert pixel column position to text cell column position.
+ *
+ * @param x pixel column position
+ * @return text cell column position
+ */
+ public int textColumn(final int x) {
+ int column = ((x - left) / textWidth);
+ if (column < 0) {
+ column = 0;
+ }
+ if (column > width - 1) {
+ column = width - 1;
+ }
+ return column;
+ }
+
+ /**
+ * Convert pixel row position to text cell row position.
+ *
+ * @param y pixel row position
+ * @return text cell row position
+ */
+ public int textRow(final int y) {
+ int row = ((y - top) / textHeight);
+ if (row < 0) {
+ row = 0;
+ }
+ if (row > height - 1) {
+ row = height - 1;
+ }
+ return row;
+ }
+
+ /**
+ * Getter for sessionInfo.
+ *
+ * @return the SessionInfo
+ */
+ public SessionInfo getSessionInfo() {
+ return sessionInfo;
+ }
+
+ /**
+ * Getter for the underlying Swing component.
+ *
+ * @return the SwingComponent
+ */
+ public SwingComponent getSwingComponent() {
+ return swing;
+ }
+
+ // ------------------------------------------------------------------------
+ // KeyListener ------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Pass Swing keystrokes into the event queue.
+ *
+ * @param key keystroke received
+ */
+ public void keyReleased(final KeyEvent key) {
+ // Ignore release events
+ }
+
+ /**
+ * Pass Swing keystrokes into the event queue.
+ *
+ * @param key keystroke received
+ */
+ public void keyTyped(final KeyEvent key) {
+ // Ignore typed events
+ }
+
+ /**
+ * Pass Swing keystrokes into the event queue.
+ *
+ * @param key keystroke received
+ */
+ public void keyPressed(final KeyEvent key) {
+ boolean alt = false;
+ boolean shift = false;
+ boolean ctrl = false;
+ char ch = ' ';
+ boolean isKey = false;
+ if (key.isActionKey()) {
+ isKey = true;
+ } else {
+ ch = key.getKeyChar();
+ }
+ alt = key.isAltDown();
+ ctrl = key.isControlDown();
+ shift = key.isShiftDown();
+
+ /*
+ System.err.printf("Swing Key: %s\n", key);
+ System.err.printf(" isKey: %s\n", isKey);
+ System.err.printf(" alt: %s\n", alt);
+ System.err.printf(" ctrl: %s\n", ctrl);
+ System.err.printf(" shift: %s\n", shift);
+ System.err.printf(" ch: %s\n", ch);
+ */
+
+ // Special case: not return the bare modifier presses
+ switch (key.getKeyCode()) {
+ case KeyEvent.VK_ALT:
+ return;
+ case KeyEvent.VK_ALT_GRAPH:
+ return;
+ case KeyEvent.VK_CONTROL:
+ return;
+ case KeyEvent.VK_SHIFT:
+ return;
+ case KeyEvent.VK_META:
+ return;
+ default:
+ break;
+ }
+
+ TKeypress keypress = null;
+ if (isKey) {
+ switch (key.getKeyCode()) {
+ case KeyEvent.VK_F1:
+ keypress = new TKeypress(true, TKeypress.F1, ' ',
+ alt, ctrl, shift);
+ break;
+ case KeyEvent.VK_F2:
+ keypress = new TKeypress(true, TKeypress.F2, ' ',
+ alt, ctrl, shift);
+ break;
+ case KeyEvent.VK_F3:
+ keypress = new TKeypress(true, TKeypress.F3, ' ',
+ alt, ctrl, shift);
+ break;
+ case KeyEvent.VK_F4:
+ keypress = new TKeypress(true, TKeypress.F4, ' ',
+ alt, ctrl, shift);
+ break;
+ case KeyEvent.VK_F5:
+ keypress = new TKeypress(true, TKeypress.F5, ' ',
+ alt, ctrl, shift);
+ break;
+ case KeyEvent.VK_F6:
+ keypress = new TKeypress(true, TKeypress.F6, ' ',
+ alt, ctrl, shift);
+ break;
+ case KeyEvent.VK_F7:
+ keypress = new TKeypress(true, TKeypress.F7, ' ',
+ alt, ctrl, shift);
+ break;
+ case KeyEvent.VK_F8:
+ keypress = new TKeypress(true, TKeypress.F8, ' ',
+ alt, ctrl, shift);
+ break;
+ case KeyEvent.VK_F9:
+ keypress = new TKeypress(true, TKeypress.F9, ' ',
+ alt, ctrl, shift);
+ break;
+ case KeyEvent.VK_F10:
+ keypress = new TKeypress(true, TKeypress.F10, ' ',
+ alt, ctrl, shift);
+ break;
+ case KeyEvent.VK_F11:
+ keypress = new TKeypress(true, TKeypress.F11, ' ',
+ alt, ctrl, shift);
+ break;
+ case KeyEvent.VK_F12:
+ keypress = new TKeypress(true, TKeypress.F12, ' ',
+ alt, ctrl, shift);
+ break;
+ case KeyEvent.VK_HOME:
+ keypress = new TKeypress(true, TKeypress.HOME, ' ',
+ alt, ctrl, shift);
+ break;
+ case KeyEvent.VK_END:
+ keypress = new TKeypress(true, TKeypress.END, ' ',
+ alt, ctrl, shift);
+ break;
+ case KeyEvent.VK_PAGE_UP:
+ keypress = new TKeypress(true, TKeypress.PGUP, ' ',
+ alt, ctrl, shift);
+ break;
+ case KeyEvent.VK_PAGE_DOWN:
+ keypress = new TKeypress(true, TKeypress.PGDN, ' ',
+ alt, ctrl, shift);
+ break;
+ case KeyEvent.VK_INSERT:
+ keypress = new TKeypress(true, TKeypress.INS, ' ',
+ alt, ctrl, shift);
+ break;
+ case KeyEvent.VK_DELETE:
+ keypress = new TKeypress(true, TKeypress.DEL, ' ',
+ alt, ctrl, shift);
+ break;
+ case KeyEvent.VK_RIGHT:
+ keypress = new TKeypress(true, TKeypress.RIGHT, ' ',
+ alt, ctrl, shift);
+ break;
+ case KeyEvent.VK_LEFT:
+ keypress = new TKeypress(true, TKeypress.LEFT, ' ',
+ alt, ctrl, shift);
+ break;
+ case KeyEvent.VK_UP:
+ keypress = new TKeypress(true, TKeypress.UP, ' ',
+ alt, ctrl, shift);
+ break;
+ case KeyEvent.VK_DOWN:
+ keypress = new TKeypress(true, TKeypress.DOWN, ' ',
+ alt, ctrl, shift);
+ break;
+ case KeyEvent.VK_TAB:
+ // Special case: distinguish TAB vs BTAB
+ if (shift) {
+ keypress = kbShiftTab;
+ } else {
+ keypress = kbTab;
+ }
+ break;
+ case KeyEvent.VK_ENTER:
+ keypress = new TKeypress(true, TKeypress.ENTER, ' ',
+ alt, ctrl, shift);
+ break;
+ case KeyEvent.VK_ESCAPE:
+ keypress = new TKeypress(true, TKeypress.ESC, ' ',
+ alt, ctrl, shift);
+ break;
+ case KeyEvent.VK_BACK_SPACE:
+ keypress = kbBackspace;
+ break;
+ default:
+ // Unsupported, ignore
+ return;
+ }
+ }
+
+ if (keypress == null) {
+ switch (ch) {
+ case 0x08:
+ // Disambiguate ^H from Backspace.
+ if (KeyEvent.getKeyText(key.getKeyCode()).equals("H")) {
+ // This is ^H.
+ keypress = kbBackspace;
+ } else {
+ // We are emulating Xterm here, where the backspace key
+ // on the keyboard returns ^?.
+ keypress = kbBackspaceDel;
+ }
+ break;
+ case 0x0A:
+ keypress = kbEnter;
+ break;
+ case 0x1B:
+ keypress = kbEsc;
+ break;
+ case 0x0D:
+ keypress = kbEnter;
+ break;
+ case 0x09:
+ if (shift) {
+ keypress = kbShiftTab;
+ } else {
+ keypress = kbTab;
+ }
+ break;
+ case 0x7F:
+ keypress = kbDel;
+ break;
+ default:
+ if (!alt && ctrl && !shift) {
+ // Control character, replace ch with 'A', 'B', etc.
+ ch = KeyEvent.getKeyText(key.getKeyCode()).charAt(0);
+ }
+ // Not a special key, put it together
+ keypress = new TKeypress(false, 0, ch, alt, ctrl, shift);
+ }
+ }
+
+ // Save it and we are done.
+ synchronized (eventQueue) {
+ eventQueue.add(new TKeypressEvent(keypress));
+ resetBlinkTimer();
+ }
+ if (listener != null) {
+ synchronized (listener) {
+ listener.notifyAll();
+ }
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // WindowListener ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Pass window events into the event queue.
+ *
+ * @param event window event received
+ */
+ public void windowActivated(final WindowEvent event) {
+ // Force a total repaint
+ synchronized (this) {
+ clearPhysical();
+ }
+ }
+
+ /**
+ * Pass window events into the event queue.
+ *
+ * @param event window event received
+ */
+ public void windowClosed(final WindowEvent event) {
+ // Ignore
+ }
+
+ /**
+ * Pass window events into the event queue.
+ *
+ * @param event window event received
+ */
+ public void windowClosing(final WindowEvent event) {
+ // Drop a cmBackendDisconnect and walk away
+ synchronized (eventQueue) {
+ eventQueue.add(new TCommandEvent(cmBackendDisconnect));
+ resetBlinkTimer();
+ }
+ if (listener != null) {
+ synchronized (listener) {
+ listener.notifyAll();
+ }
+ }
+ }
+
+ /**
+ * Pass window events into the event queue.
+ *
+ * @param event window event received
+ */
+ public void windowDeactivated(final WindowEvent event) {
+ // Ignore
+ }
+
+ /**
+ * Pass window events into the event queue.
+ *
+ * @param event window event received
+ */
+ public void windowDeiconified(final WindowEvent event) {
+ // Ignore
+ }
+
+ /**
+ * Pass window events into the event queue.
+ *
+ * @param event window event received
+ */
+ public void windowIconified(final WindowEvent event) {
+ // Ignore
+ }
+
+ /**
+ * Pass window events into the event queue.
+ *
+ * @param event window event received
+ */
+ public void windowOpened(final WindowEvent event) {
+ // Ignore
+ }
+
+ // ------------------------------------------------------------------------
+ // ComponentListener ------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Pass component events into the event queue.
+ *
+ * @param event component event received
+ */
+ public void componentHidden(final ComponentEvent event) {
+ // Ignore
+ }
+
+ /**
+ * Pass component events into the event queue.
+ *
+ * @param event component event received
+ */
+ public void componentShown(final ComponentEvent event) {
+ // Ignore
+ }
+
+ /**
+ * Pass component events into the event queue.
+ *
+ * @param event component event received
+ */
+ public void componentMoved(final ComponentEvent event) {
+ // Ignore
+ }
+
+ /**
+ * Pass component events into the event queue.
+ *
+ * @param event component event received
+ */
+ public void componentResized(final ComponentEvent event) {
+ if (gotFontDimensions == false) {
+ // We are still waiting to get font information. Don't pass a
+ // resize event up.
+ // System.err.println("size " + swing.getComponent().getSize());
+ return;
+ }
+
+ if (sessionInfo == null) {
+ // This is the initial component resize in construction, bail
+ // out.
+ return;
+ }
+
+ // Drop a new TResizeEvent into the queue
+ sessionInfo.queryWindowSize();
+ synchronized (eventQueue) {
+ TResizeEvent windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN,
+ sessionInfo.getWindowWidth(), sessionInfo.getWindowHeight());
+ eventQueue.add(windowResize);
+ resetBlinkTimer();
+ /*
+ System.err.println("Add resize event: " + windowResize.getWidth() +
+ " x " + windowResize.getHeight());
+ */
+ }
+ if (listener != null) {
+ synchronized (listener) {
+ listener.notifyAll();
+ }
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // MouseMotionListener ----------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Pass mouse events into the event queue.
+ *
+ * @param mouse mouse event received
+ */
+ public void mouseDragged(final MouseEvent mouse) {
+ int modifiers = mouse.getModifiersEx();
+ boolean eventMouse1 = false;
+ boolean eventMouse2 = false;
+ boolean eventMouse3 = false;
+ if ((modifiers & MouseEvent.BUTTON1_DOWN_MASK) != 0) {
+ eventMouse1 = true;
+ }
+ if ((modifiers & MouseEvent.BUTTON2_DOWN_MASK) != 0) {
+ eventMouse2 = true;
+ }
+ if ((modifiers & MouseEvent.BUTTON3_DOWN_MASK) != 0) {
+ eventMouse3 = true;
+ }
+ mouse1 = eventMouse1;
+ mouse2 = eventMouse2;
+ mouse3 = eventMouse3;
+ int x = textColumn(mouse.getX());
+ int y = textRow(mouse.getY());
+
+ TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_MOTION,
+ x, y, x, y, mouse1, mouse2, mouse3, false, false);
+
+ synchronized (eventQueue) {
+ eventQueue.add(mouseEvent);
+ resetBlinkTimer();
+ }
+ if (listener != null) {
+ synchronized (listener) {
+ listener.notifyAll();
+ }
+ }
+ }
+
+ /**
+ * Pass mouse events into the event queue.
+ *
+ * @param mouse mouse event received
+ */
+ public void mouseMoved(final MouseEvent mouse) {
+ int x = textColumn(mouse.getX());
+ int y = textRow(mouse.getY());
+ if ((x == oldMouseX) && (y == oldMouseY)) {
+ // Bail out, we've moved some pixels but not a whole text cell.
+ return;
+ }
+ oldMouseX = x;
+ oldMouseY = y;
+
+ TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_MOTION,
+ x, y, x, y, mouse1, mouse2, mouse3, false, false);
+
+ synchronized (eventQueue) {
+ eventQueue.add(mouseEvent);
+ resetBlinkTimer();
+ }
+ if (listener != null) {
+ synchronized (listener) {
+ listener.notifyAll();
+ }
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // MouseListener ----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Pass mouse events into the event queue.
+ *
+ * @param mouse mouse event received
+ */
+ public void mouseClicked(final MouseEvent mouse) {
+ // Ignore
+ }
+
+ /**
+ * Pass mouse events into the event queue.
+ *
+ * @param mouse mouse event received
+ */
+ public void mouseEntered(final MouseEvent mouse) {
+ swing.requestFocusInWindow();
+ }
+
+ /**
+ * Pass mouse events into the event queue.
+ *
+ * @param mouse mouse event received
+ */
+ public void mouseExited(final MouseEvent mouse) {
+ // Ignore
+ }
+
+ /**
+ * Pass mouse events into the event queue.
+ *
+ * @param mouse mouse event received
+ */
+ public void mousePressed(final MouseEvent mouse) {
+ int modifiers = mouse.getModifiersEx();
+ boolean eventMouse1 = false;
+ boolean eventMouse2 = false;
+ boolean eventMouse3 = false;
+ if ((modifiers & MouseEvent.BUTTON1_DOWN_MASK) != 0) {
+ eventMouse1 = true;
+ }
+ if ((modifiers & MouseEvent.BUTTON2_DOWN_MASK) != 0) {
+ eventMouse2 = true;
+ }
+ if ((modifiers & MouseEvent.BUTTON3_DOWN_MASK) != 0) {
+ eventMouse3 = true;
+ }
+ mouse1 = eventMouse1;
+ mouse2 = eventMouse2;
+ mouse3 = eventMouse3;
+ int x = textColumn(mouse.getX());
+ int y = textRow(mouse.getY());
+
+ TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_DOWN,
+ x, y, x, y, mouse1, mouse2, mouse3, false, false);
+
+ synchronized (eventQueue) {
+ eventQueue.add(mouseEvent);
+ resetBlinkTimer();
+ }
+ if (listener != null) {
+ synchronized (listener) {
+ listener.notifyAll();
+ }
+ }
+ }
+
+ /**
+ * Pass mouse events into the event queue.
+ *
+ * @param mouse mouse event received
+ */
+ public void mouseReleased(final MouseEvent mouse) {
+ int modifiers = mouse.getModifiersEx();
+ boolean eventMouse1 = false;
+ boolean eventMouse2 = false;
+ boolean eventMouse3 = false;
+ if ((modifiers & MouseEvent.BUTTON1_DOWN_MASK) != 0) {
+ eventMouse1 = true;
+ }
+ if ((modifiers & MouseEvent.BUTTON2_DOWN_MASK) != 0) {
+ eventMouse2 = true;
+ }
+ if ((modifiers & MouseEvent.BUTTON3_DOWN_MASK) != 0) {
+ eventMouse3 = true;
+ }
+ if (mouse1) {
+ mouse1 = false;
+ eventMouse1 = true;
+ }
+ if (mouse2) {
+ mouse2 = false;
+ eventMouse2 = true;
+ }
+ if (mouse3) {
+ mouse3 = false;
+ eventMouse3 = true;
+ }
+ int x = textColumn(mouse.getX());
+ int y = textRow(mouse.getY());
+
+ TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_UP,
+ x, y, x, y, eventMouse1, eventMouse2, eventMouse3, false, false);
+
+ synchronized (eventQueue) {
+ eventQueue.add(mouseEvent);
+ resetBlinkTimer();
+ }
+ if (listener != null) {
+ synchronized (listener) {
+ listener.notifyAll();
+ }
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // MouseWheelListener -----------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Pass mouse events into the event queue.
+ *
+ * @param mouse mouse event received
+ */
+ public void mouseWheelMoved(final MouseWheelEvent mouse) {
+ int modifiers = mouse.getModifiersEx();
+ boolean eventMouse1 = false;
+ boolean eventMouse2 = false;
+ boolean eventMouse3 = false;
+ boolean mouseWheelUp = false;
+ boolean mouseWheelDown = false;
+ if ((modifiers & MouseEvent.BUTTON1_DOWN_MASK) != 0) {
+ eventMouse1 = true;
+ }
+ if ((modifiers & MouseEvent.BUTTON2_DOWN_MASK) != 0) {
+ eventMouse2 = true;
+ }
+ if ((modifiers & MouseEvent.BUTTON3_DOWN_MASK) != 0) {
+ eventMouse3 = true;
+ }
+ mouse1 = eventMouse1;
+ mouse2 = eventMouse2;
+ mouse3 = eventMouse3;
+ int x = textColumn(mouse.getX());
+ int y = textRow(mouse.getY());
+ if (mouse.getWheelRotation() > 0) {
+ mouseWheelDown = true;
+ }
+ if (mouse.getWheelRotation() < 0) {
+ mouseWheelUp = true;
+ }
+
+ TMouseEvent mouseEvent = new TMouseEvent(TMouseEvent.Type.MOUSE_DOWN,
+ x, y, x, y, mouse1, mouse2, mouse3, mouseWheelUp, mouseWheelDown);
+
+ synchronized (eventQueue) {
+ eventQueue.add(mouseEvent);
+ resetBlinkTimer();
+ }
+ if (listener != null) {
+ synchronized (listener) {
+ listener.notifyAll();
+ }
+ }
+ }
+
+ }
--- /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;
+
+ /**
+ * TSessionInfo provides a default session implementation. The username is
+ * blank, language is "en_US", with a 80x24 text window.
+ */
+ public class TSessionInfo implements SessionInfo {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * User name.
+ */
+ private String username = "";
+
+ /**
+ * Language.
+ */
+ private String language = "en_US";
+
+ /**
+ * Text window width.
+ */
+ private int windowWidth = 80;
+
+ /**
+ * Text window height.
+ */
+ private int windowHeight = 24;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ */
+ public TSessionInfo() {
+ this(80, 24);
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param width the number of columns
+ * @param height the number of rows
+ */
+ public TSessionInfo(final int width, final int height) {
+ this.windowWidth = width;
+ this.windowHeight = height;
+ }
+
+ // ------------------------------------------------------------------------
+ // SessionInfo ------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Username getter.
+ *
+ * @return the username
+ */
+ public String getUsername() {
+ return this.username;
+ }
+
+ /**
+ * Username setter.
+ *
+ * @param username the value
+ */
+ public void setUsername(final String username) {
+ this.username = username;
+ }
+
+ /**
+ * Language getter.
+ *
+ * @return the language
+ */
+ public String getLanguage() {
+ return this.language;
+ }
+
+ /**
+ * Language setter.
+ *
+ * @param language the value
+ */
+ public void setLanguage(final String language) {
+ this.language = language;
+ }
+
+ /**
+ * Text window width getter.
+ *
+ * @return the window width
+ */
+ public int getWindowWidth() {
+ return windowWidth;
+ }
+
+ /**
+ * Text window height getter.
+ *
+ * @return the window height
+ */
+ public int getWindowHeight() {
+ return windowHeight;
+ }
+
+ /**
+ * Re-query the text window size.
+ */
+ public void queryWindowSize() {
+ // 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.backend;
+
+ import java.io.BufferedReader;
+ import java.io.InputStreamReader;
+ import java.io.IOException;
+ import java.util.StringTokenizer;
+
+ /**
+ * TTYSessionInfo queries environment variables and the tty window size for
+ * the session information. The username is taken from user.name, language
+ * is taken from user.language, and text window size from 'stty size'.
+ */
+ public class TTYSessionInfo implements SessionInfo {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * User name.
+ */
+ private String username = "";
+
+ /**
+ * Language.
+ */
+ private String language = "";
+
+ /**
+ * Text window width. Default is 80x24 (same as VT100-ish terminals).
+ */
+ private int windowWidth = 80;
+
+ /**
+ * Text window height. Default is 80x24 (same as VT100-ish terminals).
+ */
+ private int windowHeight = 24;
+
+ /**
+ * Time at which the window size was refreshed.
+ */
+ private long lastQueryWindowTime;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ */
+ public TTYSessionInfo() {
+ // Populate lang and user from the environment
+ username = System.getProperty("user.name");
+ language = System.getProperty("user.language");
+ queryWindowSize();
+ }
+
+ // ------------------------------------------------------------------------
+ // SessionInfo ------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Username getter.
+ *
+ * @return the username
+ */
+ public String getUsername() {
+ return this.username;
+ }
+
+ /**
+ * Username setter.
+ *
+ * @param username the value
+ */
+ public void setUsername(final String username) {
+ this.username = username;
+ }
+
+ /**
+ * Language getter.
+ *
+ * @return the language
+ */
+ public String getLanguage() {
+ return this.language;
+ }
+
+ /**
+ * Language setter.
+ *
+ * @param language the value
+ */
+ public void setLanguage(final String language) {
+ this.language = language;
+ }
+
+ /**
+ * Text window width getter.
+ *
+ * @return the window width
+ */
+ public int getWindowWidth() {
+ if (System.getProperty("os.name").startsWith("Windows")) {
+ // Always use 80x25 for Windows (same as DOS)
+ return 80;
+ }
+ return windowWidth;
+ }
+
+ /**
+ * Text window height getter.
+ *
+ * @return the window height
+ */
+ public int getWindowHeight() {
+ if (System.getProperty("os.name").startsWith("Windows")) {
+ // Always use 80x25 for Windows (same as DOS)
+ return 25;
+ }
+ return windowHeight;
+ }
+
+ /**
+ * Re-query the text window size.
+ */
+ public void queryWindowSize() {
+ if (lastQueryWindowTime == 0) {
+ lastQueryWindowTime = System.currentTimeMillis();
+ } else {
+ long nowTime = System.currentTimeMillis();
+ if (nowTime - lastQueryWindowTime < 1000) {
+ // Don't re-spawn stty if it hasn't been a full second since
+ // the last time.
+ return;
+ }
+ }
+ if (System.getProperty("os.name").startsWith("Linux")
+ || System.getProperty("os.name").startsWith("Mac OS X")
+ || System.getProperty("os.name").startsWith("SunOS")
+ || System.getProperty("os.name").startsWith("FreeBSD")
+ ) {
+ // Use stty to get the window size
+ sttyWindowSize();
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // TTYSessionInfo ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Call 'stty size' to obtain the tty window size. windowWidth and
+ * windowHeight are set automatically.
+ */
+ private void sttyWindowSize() {
+ String [] cmd = {
+ "/bin/sh", "-c", "stty size < /dev/tty"
+ };
+ try {
+ Process process = Runtime.getRuntime().exec(cmd);
+ BufferedReader in = new BufferedReader(
+ new InputStreamReader(process.getInputStream(), "UTF-8"));
+ String line = in.readLine();
+ if ((line != null) && (line.length() > 0)) {
+ StringTokenizer tokenizer = new StringTokenizer(line);
+ int rc = Integer.parseInt(tokenizer.nextToken());
+ if (rc > 0) {
+ windowHeight = rc;
+ }
+ rc = Integer.parseInt(tokenizer.nextToken());
+ if (rc > 0) {
+ windowWidth = rc;
+ }
+ }
+ while (true) {
+ BufferedReader err = new BufferedReader(
+ new InputStreamReader(process.getErrorStream(),
+ "UTF-8"));
+ line = err.readLine();
+ if ((line != null) && (line.length() > 0)) {
+ System.err.println("Error output from stty: " + line);
+ }
+ try {
+ process.waitFor();
+ break;
+ } catch (InterruptedException e) {
+ // SQUASH
+ }
+ }
+ int rc = process.exitValue();
+ if (rc != 0) {
+ System.err.println("stty returned error code: " + rc);
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ }
--- /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.ArrayList;
+ import java.util.List;
+
+ import jexer.TApplication;
+ import jexer.TWindow;
+ import jexer.event.TCommandEvent;
+ import jexer.event.TInputEvent;
+ import jexer.event.TKeypressEvent;
+ import jexer.event.TMouseEvent;
+ import jexer.event.TResizeEvent;
+ import static jexer.TCommand.*;
+
+ /**
+ * TWindowBackend uses a window in one TApplication to provide a backend for
+ * another TApplication.
+ *
+ * Note that TWindow has its own getScreen() and setTitle() functions.
+ * Clients in TWindowBackend's application won't be able to use it to get at
+ * the other application's screen. getOtherScreen() has been provided.
+ */
+ public class TWindowBackend extends TWindow implements Backend {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The listening object that run() wakes up on new input.
+ */
+ private Object listener;
+
+ /**
+ * The object to sync on in draw(). This is normally otherScreen, but it
+ * could also be a MultiScreen.
+ */
+ private Object drawLock;
+
+ /**
+ * The event queue, filled up by a thread reading on input.
+ */
+ private List<TInputEvent> eventQueue;
+
+ /**
+ * The screen this window is monitoring.
+ */
+ private Screen otherScreen;
+
+ /**
+ * The application associated with otherScreen.
+ */
+ private TApplication otherApplication;
+
+ /**
+ * The session information.
+ */
+ private SessionInfo sessionInfo;
+
+ /**
+ * OtherScreen provides a hook to notify TWindowBackend of screen size
+ * changes.
+ */
+ private class OtherScreen extends LogicalScreen {
+
+ /**
+ * The TWindowBackend to notify.
+ */
+ private TWindowBackend window;
+
+ /**
+ * Public constructor.
+ */
+ public OtherScreen(final TWindowBackend window) {
+ this.window = window;
+ }
+
+ /**
+ * Resize the physical screen to match the logical screen dimensions.
+ */
+ @Override
+ public void resizeToScreen() {
+ window.setWidth(getWidth() + 2);
+ window.setHeight(getHeight() + 2);
+ }
+
+ /**
+ * Get the width of a character cell in pixels.
+ *
+ * @return the width in pixels of a character cell
+ */
+ @Override
+ public int getTextWidth() {
+ return window.getScreen().getTextWidth();
+ }
+
+ /**
+ * Get the height of a character cell in pixels.
+ *
+ * @return the height in pixels of a character cell
+ */
+ @Override
+ public int getTextHeight() {
+ return window.getScreen().getTextHeight();
+ }
+
+ }
+
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor. Window will be located at (0, 0).
+ *
+ * @param listener the object this backend needs to wake up when new
+ * input comes in
+ * @param application TApplication that manages this window
+ * @param title window title, will be centered along the top border
+ * @param width width of window
+ * @param height height of window
+ */
+ public TWindowBackend(final Object listener,
+ final TApplication application, final String title,
+ final int width, final int height) {
+
+ super(application, title, width, height);
+
+ this.listener = listener;
+ eventQueue = new ArrayList<TInputEvent>();
+ sessionInfo = new TSessionInfo(width, height);
+ otherScreen = new OtherScreen(this);
+ otherScreen.setDimensions(width - 2, height - 2);
+ drawLock = otherScreen;
+ setHiddenMouse(true);
+ }
+
+ /**
+ * Public constructor. Window will be located at (0, 0).
+ *
+ * @param listener the object this backend needs to wake up when new
+ * input comes in
+ * @param application TApplication that manages this window
+ * @param title window title, will be centered along the top border
+ * @param width width of window
+ * @param height height of window
+ * @param flags bitmask of RESIZABLE, CENTERED, or MODAL
+ */
+ public TWindowBackend(final Object listener,
+ final TApplication application, final String title,
+ final int width, final int height, final int flags) {
+
+ super(application, title, width, height, flags);
+
+ this.listener = listener;
+ eventQueue = new ArrayList<TInputEvent>();
+ sessionInfo = new TSessionInfo(width, height);
+ otherScreen = new OtherScreen(this);
+ otherScreen.setDimensions(width - 2, height - 2);
+ drawLock = otherScreen;
+ setHiddenMouse(true);
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param listener the object this backend needs to wake up when new
+ * input comes in
+ * @param application TApplication that manages this window
+ * @param title window title, will be centered along the top border
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of window
+ * @param height height of window
+ */
+ public TWindowBackend(final Object listener,
+ final TApplication application, final String title,
+ final int x, final int y, final int width, final int height) {
+
+ super(application, title, x, y, width, height);
+
+ this.listener = listener;
+ eventQueue = new ArrayList<TInputEvent>();
+ sessionInfo = new TSessionInfo(width, height);
+ otherScreen = new OtherScreen(this);
+ otherScreen.setDimensions(width - 2, height - 2);
+ drawLock = otherScreen;
+ setHiddenMouse(true);
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param listener the object this backend needs to wake up when new
+ * input comes in
+ * @param application TApplication that manages this window
+ * @param title window title, will be centered along the top border
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of window
+ * @param height height of window
+ * @param flags mask of RESIZABLE, CENTERED, or MODAL
+ */
+ public TWindowBackend(final Object listener,
+ final TApplication application, final String title,
+ final int x, final int y, final int width, final int height,
+ final int flags) {
+
+ super(application, title, x, y, width, height, flags);
+
+ this.listener = listener;
+ eventQueue = new ArrayList<TInputEvent>();
+ sessionInfo = new TSessionInfo(width, height);
+ otherScreen = new OtherScreen(this);
+ otherScreen.setDimensions(width - 2, height - 2);
+ drawLock = otherScreen;
+ setHiddenMouse(true);
+ }
+
+ // ------------------------------------------------------------------------
+ // Event handlers ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Handle window/screen resize events.
+ *
+ * @param event resize event
+ */
+ @Override
+ public void onResize(final TResizeEvent event) {
+ if (event.getType() == TResizeEvent.Type.WIDGET) {
+ int newWidth = event.getWidth() - 2;
+ int newHeight = event.getHeight() - 2;
+ if ((newWidth != otherScreen.getWidth())
+ || (newHeight != otherScreen.getHeight())
+ ) {
+ // I was resized, notify the screen I am watching to match my
+ // new size.
+ synchronized (eventQueue) {
+ eventQueue.add(new TResizeEvent(TResizeEvent.Type.SCREEN,
+ newWidth, newHeight));
+ }
+ synchronized (listener) {
+ listener.notifyAll();
+ }
+ }
+ return;
+ } else {
+ super.onResize(event);
+ }
+ }
+
+ /**
+ * Returns true if the mouse is currently in the otherScreen window.
+ *
+ * @param mouse mouse event
+ * @return true if mouse is currently in the otherScreen window.
+ */
+ protected boolean mouseOnOtherScreen(final TMouseEvent mouse) {
+ if ((mouse.getY() >= 1)
+ && (mouse.getY() <= otherScreen.getHeight())
+ && (mouse.getX() >= 1)
+ && (mouse.getX() <= otherScreen.getWidth())
+ ) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Handle mouse button presses.
+ *
+ * @param mouse mouse button event
+ */
+ @Override
+ public void onMouseDown(final TMouseEvent mouse) {
+ if (mouseOnOtherScreen(mouse)) {
+ TMouseEvent event = mouse.dup();
+ event.setX(mouse.getX() - 1);
+ event.setY(mouse.getY() - 1);
+ event.setAbsoluteX(event.getX());
+ event.setAbsoluteY(event.getY());
+ synchronized (eventQueue) {
+ eventQueue.add(event);
+ }
+ synchronized (listener) {
+ listener.notifyAll();
+ }
+ }
+ super.onMouseDown(mouse);
+ }
+
+ /**
+ * Handle mouse button releases.
+ *
+ * @param mouse mouse button release event
+ */
+ @Override
+ public void onMouseUp(final TMouseEvent mouse) {
+ if (mouseOnOtherScreen(mouse)) {
+ TMouseEvent event = mouse.dup();
+ event.setX(mouse.getX() - 1);
+ event.setY(mouse.getY() - 1);
+ event.setAbsoluteX(event.getX());
+ event.setAbsoluteY(event.getY());
+ synchronized (eventQueue) {
+ eventQueue.add(event);
+ }
+ synchronized (listener) {
+ listener.notifyAll();
+ }
+ }
+ super.onMouseUp(mouse);
+ }
+
+ /**
+ * Handle mouse movements.
+ *
+ * @param mouse mouse motion event
+ */
+ @Override
+ public void onMouseMotion(final TMouseEvent mouse) {
+ if (mouseOnOtherScreen(mouse)) {
+ TMouseEvent event = mouse.dup();
+ event.setX(mouse.getX() - 1);
+ event.setY(mouse.getY() - 1);
+ event.setAbsoluteX(event.getX());
+ event.setAbsoluteY(event.getY());
+ synchronized (eventQueue) {
+ eventQueue.add(event);
+ }
+ synchronized (listener) {
+ listener.notifyAll();
+ }
+ }
+ super.onMouseMotion(mouse);
+ }
+
+ /**
+ * Handle keystrokes.
+ *
+ * @param keypress keystroke event
+ */
+ @Override
+ public void onKeypress(final TKeypressEvent keypress) {
+ TKeypressEvent event = keypress.dup();
+ synchronized (eventQueue) {
+ eventQueue.add(event);
+ }
+ synchronized (listener) {
+ listener.notifyAll();
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // TWindow ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Draw the foreground colors grid.
+ */
+ @Override
+ public void draw() {
+
+ // Sync on other screen, so that we do not draw in the middle of
+ // their screen update.
+ synchronized (drawLock) {
+ // Draw the box
+ super.draw();
+
+ // Draw every cell of the other screen
+ for (int y = 0; y < otherScreen.getHeight(); y++) {
+ for (int x = 0; x < otherScreen.getWidth(); x++) {
+ putCharXY(x + 1, y + 1, otherScreen.getCharXY(x, y));
+ }
+ }
+
+ // If their cursor is visible, draw that here too.
+ if (otherScreen.isCursorVisible()) {
+ setCursorX(otherScreen.getCursorX() + 1);
+ setCursorY(otherScreen.getCursorY() + 1);
+ setCursorVisible(true);
+ } else {
+ setCursorVisible(false);
+ }
+ }
+
+ // Check if the other application has died. If so, unset hidden
+ // mouse.
+ if (otherApplication != null) {
+ if (otherApplication.isRunning() == false) {
+ setHiddenMouse(false);
+ }
+ }
+
+ }
+
+ /**
+ * Subclasses should override this method to cleanup resources. This is
+ * called by application.closeWindow().
+ */
+ @Override
+ public void onClose() {
+ synchronized (eventQueue) {
+ eventQueue.add(new TCommandEvent(cmBackendDisconnect));
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // Backend ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Getter for sessionInfo.
+ *
+ * @return the SessionInfo
+ */
+ public final SessionInfo getSessionInfo() {
+ return sessionInfo;
+ }
+
+ /**
+ * Subclasses must provide an implementation that syncs the logical
+ * screen to the physical device.
+ */
+ public void flushScreen() {
+ getApplication().doRepaint();
+ }
+
+ /**
+ * Check if there are events in the queue.
+ *
+ * @return if true, getEvents() has something to return to the application
+ */
+ public boolean hasEvents() {
+ synchronized (eventQueue) {
+ return (eventQueue.size() > 0);
+ }
+ }
+
+ /**
+ * 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) {
+ synchronized (eventQueue) {
+ if (eventQueue.size() > 0) {
+ synchronized (queue) {
+ queue.addAll(eventQueue);
+ }
+ eventQueue.clear();
+ }
+ }
+ }
+
+ /**
+ * 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) {
+ this.listener = listener;
+ }
+
+ /**
+ * Reload backend options from System properties.
+ */
+ public void reloadOptions() {
+ // NOP
+ }
+
+ // ------------------------------------------------------------------------
+ // TWindowBackend ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Set the object to sync to in draw().
+ *
+ * @param drawLock the object to synchronize on
+ */
+ public void setDrawLock(final Object drawLock) {
+ this.drawLock = drawLock;
+ }
+
+ /**
+ * Getter for the other application's screen.
+ *
+ * @return the Screen
+ */
+ public Screen getOtherScreen() {
+ return otherScreen;
+ }
+
+ /**
+ * Set the other screen's application.
+ *
+ * @param application the application driving the other screen
+ */
+ public void setOtherApplication(final TApplication application) {
+ this.otherApplication = application;
+ }
+
+ }
--- /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;
+
+ /**
+ * TerminalReader provides keyboard and mouse events.
+ */
+ public interface TerminalReader {
+
+ /**
+ * Check if there are events in the queue.
+ *
+ * @return if true, getEvents() has something to return to the backend
+ */
+ public boolean hasEvents();
+
+ /**
+ * Classes 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);
+
+ /**
+ * Classes must provide an implementation that closes sockets, restores
+ * console, etc.
+ */
+ public void closeTerminal();
+
+ /**
+ * 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);
+
+ /**
+ * Reload options from System properties.
+ */
+ public void reloadOptions();
+
+ }
--- /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
+ */
+
+ /**
+ * The interface between TApplication and user-facing I/O.
+ */
+ package jexer.backend;
--- /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.Color;
+ import java.awt.image.BufferedImage;
+
+ /**
+ * This class represents a single text cell or bit of image on the screen.
+ */
+ public final class Cell extends CellAttributes {
+
+ // ------------------------------------------------------------------------
+ // Constants --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * How this cell needs to be displayed if it is part of a larger glyph.
+ */
+ public enum Width {
+ /**
+ * This cell is an entire glyph on its own.
+ */
+ SINGLE,
+
+ /**
+ * This cell is the left half of a wide glyph.
+ */
+ LEFT,
+
+ /**
+ * This cell is the right half of a wide glyph.
+ */
+ RIGHT,
+ }
+
+ /**
+ * The special "this cell is unset" (null) value. This is the Unicode
+ * "not a character" value.
+ */
+ private static final char UNSET_VALUE = (char) 65535;
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The character at this cell.
+ */
+ private int ch = ' ';
+
+ /**
+ * The display width of this cell.
+ */
+ private Width width = Width.SINGLE;
+
+ /**
+ * The image at this cell.
+ */
+ private BufferedImage image = null;
+
+ /**
+ * The image at this cell, inverted.
+ */
+ private BufferedImage invertedImage = null;
+
+ /**
+ * The background color used for the area the image portion might not
+ * cover.
+ */
+ private Color background = Color.BLACK;
+
+ /**
+ * hashCode() needs to call image.hashCode(), which can get quite
+ * expensive.
+ */
+ private int imageHashCode = 0;
+
+ /**
+ * hashCode() needs to call background.hashCode(), which can get quite
+ * expensive.
+ */
+ private int backgroundHashCode = 0;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor sets default values of the cell to blank.
+ *
+ * @see #isBlank()
+ * @see #reset()
+ */
+ public Cell() {
+ // NOP
+ }
+
+ /**
+ * Public constructor sets the character. Attributes are the same as
+ * default.
+ *
+ * @param ch character to set to
+ * @see #reset()
+ */
+ public Cell(final int ch) {
+ this.ch = ch;
+ }
+
+ /**
+ * Public constructor sets the attributes.
+ *
+ * @param attr attributes to use
+ */
+ public Cell(final CellAttributes attr) {
+ super(attr);
+ }
+
+ /**
+ * Public constructor sets the character and attributes.
+ *
+ * @param ch character to set to
+ * @param attr attributes to use
+ */
+ public Cell(final int ch, final CellAttributes attr) {
+ super(attr);
+ this.ch = ch;
+ }
+
+ /**
+ * Public constructor creates a duplicate.
+ *
+ * @param cell the instance to copy
+ */
+ public Cell(final Cell cell) {
+ setTo(cell);
+ }
+
+ // ------------------------------------------------------------------------
+ // Cell -------------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Set the image data for this cell.
+ *
+ * @param image the image for this cell
+ */
+ public void setImage(final BufferedImage image) {
+ this.image = image;
+ imageHashCode = image.hashCode();
+ width = Width.SINGLE;
+ }
+
+ /**
+ * Get the image data for this cell.
+ *
+ * @return the image for this cell
+ */
+ public BufferedImage getImage() {
+ if (invertedImage != null) {
+ return invertedImage;
+ }
+ return image;
+ }
+
+ /**
+ * Get the bitmap image background color for this cell.
+ *
+ * @return the bitmap image background color
+ */
+ public Color getBackground() {
+ return background;
+ }
+
+ /**
+ * If true, this cell has image data.
+ *
+ * @return true if this cell is an image rather than a character with
+ * attributes
+ */
+ public boolean isImage() {
+ if (image != null) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Restore the image in this cell to its normal version, if it has one.
+ */
+ public void restoreImage() {
+ invertedImage = null;
+ }
+
+ /**
+ * If true, this cell has image data, and that data is inverted.
+ *
+ * @return true if this cell is an image rather than a character with
+ * attributes, and the data is inverted
+ */
+ public boolean isInvertedImage() {
+ if ((image != null) && (invertedImage != null)) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Invert the image in this cell, if it has one.
+ */
+ public void invertImage() {
+ if (image == null) {
+ return;
+ }
+ if (invertedImage == null) {
+ invertedImage = new BufferedImage(image.getWidth(),
+ image.getHeight(), BufferedImage.TYPE_INT_ARGB);
+
+ int [] rgbArray = image.getRGB(0, 0,
+ image.getWidth(), image.getHeight(), null, 0, image.getWidth());
+
+ for (int i = 0; i < rgbArray.length; i++) {
+ // Set the colors to fully inverted.
+ if (rgbArray[i] != 0x00FFFFFF) {
+ rgbArray[i] ^= 0x00FFFFFF;
+ }
+ // Also set alpha to non-transparent.
+ rgbArray[i] |= 0xFF000000;
+ }
+ invertedImage.setRGB(0, 0, image.getWidth(), image.getHeight(),
+ rgbArray, 0, image.getWidth());
+ }
+ }
+
+ /**
+ * Getter for cell character.
+ *
+ * @return cell character
+ */
+ public int getChar() {
+ return ch;
+ }
+
+ /**
+ * Setter for cell character.
+ *
+ * @param ch new cell character
+ */
+ public void setChar(final int ch) {
+ this.ch = ch;
+ }
+
+ /**
+ * Getter for cell width.
+ *
+ * @return Width.SINGLE, Width.LEFT, or Width.RIGHT
+ */
+ public Width getWidth() {
+ return width;
+ }
+
+ /**
+ * Setter for cell width.
+ *
+ * @param width new cell width, one of Width.SINGLE, Width.LEFT, or
+ * Width.RIGHT
+ */
+ public void setWidth(final Width width) {
+ this.width = width;
+ }
+
+ /**
+ * Reset this cell to a blank.
+ */
+ @Override
+ public void reset() {
+ super.reset();
+ ch = ' ';
+ width = Width.SINGLE;
+ image = null;
+ imageHashCode = 0;
+ invertedImage = null;
+ background = Color.BLACK;
+ backgroundHashCode = 0;
+ }
+
+ /**
+ * UNset this cell. It will not be equal to any other cell until it has
+ * been assigned attributes and a character.
+ */
+ public void unset() {
+ super.reset();
+ ch = UNSET_VALUE;
+ width = Width.SINGLE;
+ image = null;
+ imageHashCode = 0;
+ invertedImage = null;
+ background = Color.BLACK;
+ backgroundHashCode = 0;
+ }
+
+ /**
+ * Check to see if this cell has default attributes: white foreground,
+ * black background, no bold/blink/reverse/underline/protect, and a
+ * character value of ' ' (space).
+ *
+ * @return true if this cell has default attributes.
+ */
+ public boolean isBlank() {
+ if ((ch == UNSET_VALUE) || (image != null)) {
+ return false;
+ }
+ if ((getForeColor().equals(Color.WHITE))
+ && (getBackColor().equals(Color.BLACK))
+ && !isBold()
+ && !isBlink()
+ && !isReverse()
+ && !isUnderline()
+ && !isProtect()
+ && !isRGB()
+ && !isImage()
+ && (width == Width.SINGLE)
+ && (ch == ' ')
+ ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Comparison check. All fields must match to return true.
+ *
+ * @param rhs another Cell instance
+ * @return true if all fields are equal
+ */
+ @Override
+ public boolean equals(final Object rhs) {
+ if (!(rhs instanceof Cell)) {
+ return false;
+ }
+
+ Cell that = (Cell) rhs;
+
+ // Unsetted cells can never be equal.
+ if ((ch == UNSET_VALUE) || (that.ch == UNSET_VALUE)) {
+ return false;
+ }
+
+ // If this or rhs has an image and the other doesn't, these are not
+ // equal.
+ if ((image != null) && (that.image == null)) {
+ return false;
+ }
+ if ((image == null) && (that.image != null)) {
+ return false;
+ }
+ // If this and rhs have images, both must match.
+ if ((image != null) && (that.image != null)) {
+ if ((invertedImage == null) && (that.invertedImage != null)) {
+ return false;
+ }
+ if ((invertedImage != null) && (that.invertedImage == null)) {
+ return false;
+ }
+ // Either both objects have their image inverted, or neither do.
+ // Now if the images are identical the cells are the same
+ // visually.
+ if (image.equals(that.image)
+ && (background.equals(that.background))
+ ) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ // Normal case: character and attributes must match.
+ if ((ch == that.ch) && (width == that.width)) {
+ return super.equals(rhs);
+ }
+ return false;
+ }
+
+ /**
+ * Hashcode uses all fields in equals().
+ *
+ * @return the hash
+ */
+ @Override
+ public int hashCode() {
+ int A = 13;
+ int B = 23;
+ int hash = A;
+ hash = (B * hash) + super.hashCode();
+ hash = (B * hash) + (int)ch;
+ hash = (B * hash) + width.hashCode();
+ if (image != null) {
+ /*
+ hash = (B * hash) + image.hashCode();
+ hash = (B * hash) + background.hashCode();
+ */
+ hash = (B * hash) + imageHashCode;
+ hash = (B * hash) + backgroundHashCode;
+ }
+ if (invertedImage != null) {
+ hash = (B * hash) + invertedImage.hashCode();
+ }
+ return hash;
+ }
+
+ /**
+ * Set my field values to that's field.
+ *
+ * @param rhs an instance of either Cell or CellAttributes
+ */
+ @Override
+ public void setTo(final Object rhs) {
+ // Let this throw a ClassCastException
+ CellAttributes thatAttr = (CellAttributes) rhs;
+ this.image = null;
+ this.imageHashCode = 0;
+ this.backgroundHashCode = 0;
+ this.width = Width.SINGLE;
+ super.setTo(thatAttr);
+
+ if (rhs instanceof Cell) {
+ Cell that = (Cell) rhs;
+ this.ch = that.ch;
+ this.width = that.width;
+ this.image = that.image;
+ this.invertedImage = that.invertedImage;
+ this.background = that.background;
+ this.imageHashCode = that.imageHashCode;
+ this.backgroundHashCode = that.backgroundHashCode;
+ }
+ }
+
+ /**
+ * Set my field attr values to that's field.
+ *
+ * @param that a CellAttributes instance
+ */
+ public void setAttr(final CellAttributes that) {
+ image = null;
+ super.setTo(that);
+ }
+
+ /**
+ * Make human-readable description of this Cell.
+ *
+ * @return displayable String
+ */
+ @Override
+ public String toString() {
+ return String.format("fore: %s back: %s bold: %s blink: %s ch %c",
+ getForeColor(), getBackColor(), isBold(), isBlink(), ch);
+ }
+ }
--- /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;
+
+ /**
+ * The attributes used by a Cell: color, bold, blink, etc.
+ */
+ public class CellAttributes {
+
+ // ------------------------------------------------------------------------
+ // Constants --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Bold attribute.
+ */
+ private static final int BOLD = 0x01;
+
+ /**
+ * Blink attribute.
+ */
+ private static final int BLINK = 0x02;
+
+ /**
+ * Reverse attribute.
+ */
+ private static final int REVERSE = 0x04;
+
+ /**
+ * Underline attribute.
+ */
+ private static final int UNDERLINE = 0x08;
+
+ /**
+ * Protected attribute.
+ */
+ private static final int PROTECT = 0x10;
+
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Boolean flags.
+ */
+ private int flags = 0;
+
+ /**
+ * Foreground color. Color.WHITE, Color.RED, etc.
+ */
+ private Color foreColor = Color.WHITE;
+
+ /**
+ * Background color. Color.WHITE, Color.RED, etc.
+ */
+ private Color backColor = Color.BLACK;
+
+ /**
+ * Foreground color as 24-bit RGB value. Negative value means not set.
+ */
+ private int foreColorRGB = -1;
+
+ /**
+ * Background color as 24-bit RGB value. Negative value means not set.
+ */
+ private int backColorRGB = -1;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor sets default values of the cell to white-on-black,
+ * no bold/blink/reverse/underline/protect.
+ *
+ * @see #reset()
+ */
+ public CellAttributes() {
+ // NOP
+ }
+
+ /**
+ * Public constructor makes a copy from another instance.
+ *
+ * @param that another CellAttributes instance
+ * @see #reset()
+ */
+ public CellAttributes(final CellAttributes that) {
+ setTo(that);
+ }
+
+ // ------------------------------------------------------------------------
+ // CellAttributes ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Getter for bold.
+ *
+ * @return bold value
+ */
+ public final boolean isBold() {
+ return ((flags & BOLD) == 0 ? false : true);
+ }
+
+ /**
+ * Setter for bold.
+ *
+ * @param bold new bold value
+ */
+ public final void setBold(final boolean bold) {
+ if (bold) {
+ flags |= BOLD;
+ } else {
+ flags &= ~BOLD;
+ }
+ }
+
+ /**
+ * Getter for blink.
+ *
+ * @return blink value
+ */
+ public final boolean isBlink() {
+ return ((flags & BLINK) == 0 ? false : true);
+ }
+
+ /**
+ * Setter for blink.
+ *
+ * @param blink new blink value
+ */
+ public final void setBlink(final boolean blink) {
+ if (blink) {
+ flags |= BLINK;
+ } else {
+ flags &= ~BLINK;
+ }
+ }
+
+ /**
+ * Getter for reverse.
+ *
+ * @return reverse value
+ */
+ public final boolean isReverse() {
+ return ((flags & REVERSE) == 0 ? false : true);
+ }
+
+ /**
+ * Setter for reverse.
+ *
+ * @param reverse new reverse value
+ */
+ public final void setReverse(final boolean reverse) {
+ if (reverse) {
+ flags |= REVERSE;
+ } else {
+ flags &= ~REVERSE;
+ }
+ }
+
+ /**
+ * Getter for underline.
+ *
+ * @return underline value
+ */
+ public final boolean isUnderline() {
+ return ((flags & UNDERLINE) == 0 ? false : true);
+ }
+
+ /**
+ * Setter for underline.
+ *
+ * @param underline new underline value
+ */
+ public final void setUnderline(final boolean underline) {
+ if (underline) {
+ flags |= UNDERLINE;
+ } else {
+ flags &= ~UNDERLINE;
+ }
+ }
+
+ /**
+ * Getter for protect.
+ *
+ * @return protect value
+ */
+ public final boolean isProtect() {
+ return ((flags & PROTECT) == 0 ? false : true);
+ }
+
+ /**
+ * Setter for protect.
+ *
+ * @param protect new protect value
+ */
+ public final void setProtect(final boolean protect) {
+ if (protect) {
+ flags |= PROTECT;
+ } else {
+ flags &= ~PROTECT;
+ }
+ }
+
+ /**
+ * Getter for foreColor.
+ *
+ * @return foreColor value
+ */
+ public final Color getForeColor() {
+ return foreColor;
+ }
+
+ /**
+ * Setter for foreColor.
+ *
+ * @param foreColor new foreColor value
+ */
+ public final void setForeColor(final Color foreColor) {
+ this.foreColor = foreColor;
+ }
+
+ /**
+ * Getter for backColor.
+ *
+ * @return backColor value
+ */
+ public final Color getBackColor() {
+ return backColor;
+ }
+
+ /**
+ * Setter for backColor.
+ *
+ * @param backColor new backColor value
+ */
+ public final void setBackColor(final Color backColor) {
+ this.backColor = backColor;
+ }
+
+ /**
+ * Getter for foreColor RGB.
+ *
+ * @return foreColor value. Negative means unset.
+ */
+ public final int getForeColorRGB() {
+ return foreColorRGB;
+ }
+
+ /**
+ * Setter for foreColor RGB.
+ *
+ * @param foreColorRGB new foreColor RGB value
+ */
+ public final void setForeColorRGB(final int foreColorRGB) {
+ this.foreColorRGB = foreColorRGB;
+ }
+
+ /**
+ * Getter for backColor RGB.
+ *
+ * @return backColor value. Negative means unset.
+ */
+ public final int getBackColorRGB() {
+ return backColorRGB;
+ }
+
+ /**
+ * Setter for backColor RGB.
+ *
+ * @param backColorRGB new backColor RGB value
+ */
+ public final void setBackColorRGB(final int backColorRGB) {
+ this.backColorRGB = backColorRGB;
+ }
+
+ /**
+ * See if this cell uses RGB or ANSI colors.
+ *
+ * @return true if this cell has a RGB color
+ */
+ public final boolean isRGB() {
+ return (foreColorRGB >= 0) || (backColorRGB >= 0);
+ }
+
+ /**
+ * Set to default: white foreground on black background, no
+ * bold/underline/blink/rever/protect.
+ */
+ public void reset() {
+ flags = 0;
+ foreColor = Color.WHITE;
+ backColor = Color.BLACK;
+ foreColorRGB = -1;
+ backColorRGB = -1;
+ }
+
+ /**
+ * Comparison check. All fields must match to return true.
+ *
+ * @param rhs another CellAttributes instance
+ * @return true if all fields are equal
+ */
+ @Override
+ public boolean equals(final Object rhs) {
+ if (!(rhs instanceof CellAttributes)) {
+ return false;
+ }
+
+ CellAttributes that = (CellAttributes) rhs;
+ return ((flags == that.flags)
+ && (foreColor == that.foreColor)
+ && (backColor == that.backColor)
+ && (foreColorRGB == that.foreColorRGB)
+ && (backColorRGB == that.backColorRGB));
+ }
+
+ /**
+ * Hashcode uses all fields in equals().
+ *
+ * @return the hash
+ */
+ @Override
+ public int hashCode() {
+ int A = 13;
+ int B = 23;
+ int hash = A;
+ hash = (B * hash) + flags;
+ hash = (B * hash) + foreColor.hashCode();
+ hash = (B * hash) + backColor.hashCode();
+ hash = (B * hash) + foreColorRGB;
+ hash = (B * hash) + backColorRGB;
+ return hash;
+ }
+
+ /**
+ * Set my field values to that's field.
+ *
+ * @param rhs another CellAttributes instance
+ */
+ public void setTo(final Object rhs) {
+ CellAttributes that = (CellAttributes) rhs;
+
+ this.flags = that.flags;
+ this.foreColor = that.foreColor;
+ this.backColor = that.backColor;
+ this.foreColorRGB = that.foreColorRGB;
+ this.backColorRGB = that.backColorRGB;
+ }
+
+ /**
+ * Make human-readable description of this CellAttributes.
+ *
+ * @return displayable String
+ */
+ @Override
+ public String toString() {
+ if ((foreColorRGB >= 0) || (backColorRGB >= 0)) {
+ return String.format("RGB: #%06x on #%06x",
+ (foreColorRGB & 0xFFFFFF),
+ (backColorRGB & 0xFFFFFF));
+ }
+ return String.format("%s%s%s on %s", (isBold() ? "bold " : ""),
+ (isBlink() ? "blink " : ""), foreColor, backColor);
+ }
+
+ }
--- /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;
+
+ /**
+ * A text cell color.
+ */
+ public final class Color {
+
+ // ------------------------------------------------------------------------
+ // Constants --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * SGR black value = 0.
+ */
+ private static final int SGRBLACK = 0;
+
+ /**
+ * SGR red value = 1.
+ */
+ private static final int SGRRED = 1;
+
+ /**
+ * SGR green value = 2.
+ */
+ private static final int SGRGREEN = 2;
+
+ /**
+ * SGR yellow value = 3.
+ */
+ private static final int SGRYELLOW = 3;
+
+ /**
+ * SGR blue value = 4.
+ */
+ private static final int SGRBLUE = 4;
+
+ /**
+ * SGR magenta value = 5.
+ */
+ private static final int SGRMAGENTA = 5;
+
+ /**
+ * SGR cyan value = 6.
+ */
+ private static final int SGRCYAN = 6;
+
+ /**
+ * SGR white value = 7.
+ */
+ private static final int SGRWHITE = 7;
+
+ /**
+ * Black. Bold + black = dark grey
+ */
+ public static final Color BLACK = new Color(SGRBLACK);
+
+ /**
+ * Red.
+ */
+ public static final Color RED = new Color(SGRRED);
+
+ /**
+ * Green.
+ */
+ public static final Color GREEN = new Color(SGRGREEN);
+
+ /**
+ * Yellow. Sometimes not-bold yellow is brown.
+ */
+ public static final Color YELLOW = new Color(SGRYELLOW);
+
+ /**
+ * Blue.
+ */
+ public static final Color BLUE = new Color(SGRBLUE);
+
+ /**
+ * Magenta (purple).
+ */
+ public static final Color MAGENTA = new Color(SGRMAGENTA);
+
+ /**
+ * Cyan (blue-green).
+ */
+ public static final Color CYAN = new Color(SGRCYAN);
+
+ /**
+ * White.
+ */
+ public static final Color WHITE = new Color(SGRWHITE);
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The color value. Default is SGRWHITE.
+ */
+ private int value = SGRWHITE;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Private constructor used to make the static Color instances.
+ *
+ * @param value the integer Color value
+ */
+ private Color(final int value) {
+ this.value = value;
+ }
+
+ // ------------------------------------------------------------------------
+ // Color ------------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get color value. Note that these deliberately match the color values
+ * of the ECMA-48 / ANSI X3.64 / VT100-ish SGR function ("ANSI colors").
+ *
+ * @return the value
+ */
+ public int getValue() {
+ return value;
+ }
+
+ /**
+ * Public constructor returns one of the static Color instances.
+ *
+ * @param colorName "red", "blue", etc.
+ * @return Color.RED, Color.BLUE, etc.
+ */
+ static Color getColor(final String colorName) {
+ String str = colorName.toLowerCase();
+
+ if (str.equals("black")) {
+ return Color.BLACK;
+ } else if (str.equals("white")) {
+ return Color.WHITE;
+ } else if (str.equals("red")) {
+ return Color.RED;
+ } else if (str.equals("cyan")) {
+ return Color.CYAN;
+ } else if (str.equals("green")) {
+ return Color.GREEN;
+ } else if (str.equals("magenta")) {
+ return Color.MAGENTA;
+ } else if (str.equals("blue")) {
+ return Color.BLUE;
+ } else if (str.equals("yellow")) {
+ return Color.YELLOW;
+ } else if (str.equals("brown")) {
+ return Color.YELLOW;
+ } else {
+ // Let unknown strings become white
+ return Color.WHITE;
+ }
+ }
+
+ /**
+ * Invert a color in the same way as (CGA/VGA color XOR 0x7).
+ *
+ * @return the inverted color
+ */
+ public Color invert() {
+ switch (value) {
+ case SGRBLACK:
+ return Color.WHITE;
+ case SGRWHITE:
+ return Color.BLACK;
+ case SGRRED:
+ return Color.CYAN;
+ case SGRCYAN:
+ return Color.RED;
+ case SGRGREEN:
+ return Color.MAGENTA;
+ case SGRMAGENTA:
+ return Color.GREEN;
+ case SGRBLUE:
+ return Color.YELLOW;
+ case SGRYELLOW:
+ return Color.BLUE;
+ default:
+ throw new IllegalArgumentException("Invalid Color value: " + value);
+ }
+ }
+
+ /**
+ * Comparison check. All fields must match to return true.
+ *
+ * @param rhs another Color instance
+ * @return true if all fields are equal
+ */
+ @Override
+ public boolean equals(final Object rhs) {
+ if (!(rhs instanceof Color)) {
+ return false;
+ }
+
+ Color that = (Color) rhs;
+ return (value == that.value);
+ }
+
+ /**
+ * Hashcode uses all fields in equals().
+ *
+ * @return the hash
+ */
+ @Override
+ public int hashCode() {
+ return value;
+ }
+
+ /**
+ * Make human-readable description of this Color.
+ *
+ * @return displayable String "red", "blue", etc.
+ */
+ @Override
+ public String toString() {
+ switch (value) {
+ case SGRBLACK:
+ return "black";
+ case SGRWHITE:
+ return "white";
+ case SGRRED:
+ return "red";
+ case SGRCYAN:
+ return "cyan";
+ case SGRGREEN:
+ return "green";
+ case SGRMAGENTA:
+ return "magenta";
+ case SGRBLUE:
+ return "blue";
+ case SGRYELLOW:
+ return "yellow";
+ default:
+ throw new IllegalArgumentException("Invalid Color value: " + value);
+ }
+ }
+
+ }
--- /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.io.BufferedReader;
+ import java.io.FileReader;
+ import java.io.FileWriter;
+ import java.io.IOException;
+ import java.io.Reader;
+ import java.util.ArrayList;
+ import java.util.List;
+ import java.util.Set;
+ import java.util.SortedMap;
+ import java.util.StringTokenizer;
+ import java.util.TreeMap;
+
+ /**
+ * ColorTheme is a collection of colors keyed by string. A default theme is
+ * also provided that matches the blue-and-white theme used by Turbo Vision.
+ */
+ public class ColorTheme {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The current theme colors.
+ */
+ private SortedMap<String, CellAttributes> colors;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor sets the theme to the default.
+ */
+ public ColorTheme() {
+ colors = new TreeMap<String, CellAttributes>();
+ setDefaultTheme();
+ }
+
+ // ------------------------------------------------------------------------
+ // ColorTheme -------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Retrieve the CellAttributes for a named theme color.
+ *
+ * @param name theme color name, e.g. "twindow.border"
+ * @return color associated with name, e.g. bold yellow on blue
+ */
+ public CellAttributes getColor(final String name) {
+ CellAttributes attr = colors.get(name);
+ return attr;
+ }
+
+ /**
+ * Retrieve all the names in the theme.
+ *
+ * @return a list of names
+ */
+ public List<String> getColorNames() {
+ Set<String> keys = colors.keySet();
+ List<String> names = new ArrayList<String>(keys.size());
+ names.addAll(keys);
+ return names;
+ }
+
+ /**
+ * Set the color for a named theme color.
+ *
+ * @param name theme color name, e.g. "twindow.border"
+ * @param color the new color to associate with name, e.g. bold yellow on
+ * blue
+ */
+ public void setColor(final String name, final CellAttributes color) {
+ colors.put(name, color);
+ }
+
+ /**
+ * Save the color theme mappings to an ASCII file.
+ *
+ * @param filename file to write to
+ * @throws IOException if the I/O fails
+ */
+ public void save(final String filename) throws IOException {
+ FileWriter file = new FileWriter(filename);
+ for (String key: colors.keySet()) {
+ CellAttributes color = getColor(key);
+ file.write(String.format("%s = %s\n", key, color));
+ }
+ file.close();
+ }
+
+ /**
+ * Read color theme mappings from an ASCII file.
+ *
+ * @param filename file to read from
+ * @throws IOException if the I/O fails
+ */
+ public void load(final String filename) throws IOException {
+ load(new FileReader(filename));
+ }
+
+ /**
+ * Set a color based on a text string. Color text string is of the form:
+ * <code>[ bold ] [ blink ] { foreground on background }</code>
+ *
+ * @param key the color key string
+ * @param text the text string
+ */
+ public void setColorFromString(final String key, final String text) {
+ boolean bold = false;
+ boolean blink = false;
+ String foreColor;
+ String backColor;
+ String token;
+
+ StringTokenizer tokenizer = new StringTokenizer(text);
+ token = tokenizer.nextToken();
+
+ if (token.toLowerCase().equals("rgb:")) {
+ // Foreground
+ int foreColorRGB = -1;
+ try {
+ foreColorRGB = Integer.parseInt(tokenizer.nextToken(), 16);
+ } catch (NumberFormatException e) {
+ // Default to white on black
+ foreColorRGB = 0xFFFFFF;
+ }
+
+ // "on"
+ if (!tokenizer.nextToken().toLowerCase().equals("on")) {
+ // Invalid line.
+ return;
+ }
+
+ // Background
+ int backColorRGB = -1;
+ try {
+ backColorRGB = Integer.parseInt(tokenizer.nextToken(), 16);
+ } catch (NumberFormatException e) {
+ backColorRGB = 0;
+ }
+
+ CellAttributes color = new CellAttributes();
+ color.setForeColorRGB(foreColorRGB);
+ color.setBackColorRGB(backColorRGB);
+ colors.put(key, color);
+ return;
+ }
+
+ while (token.equals("bold") || token.equals("blink")) {
+ if (token.equals("bold")) {
+ bold = true;
+ token = tokenizer.nextToken();
+ }
+ if (token.equals("blink")) {
+ blink = true;
+ token = tokenizer.nextToken();
+ }
+ }
+
+ // What's left is "blah on blah"
+ foreColor = token.toLowerCase();
+
+ if (!tokenizer.nextToken().toLowerCase().equals("on")) {
+ // Invalid line.
+ return;
+ }
+ backColor = tokenizer.nextToken().toLowerCase();
+
+ CellAttributes color = new CellAttributes();
+ if (bold) {
+ color.setBold(true);
+ }
+ if (blink) {
+ color.setBlink(true);
+ }
+ color.setForeColor(Color.getColor(foreColor));
+ color.setBackColor(Color.getColor(backColor));
+ colors.put(key, color);
+ }
+
+ /**
+ * Read color theme mappings from a Reader. The reader is closed at the
+ * end.
+ *
+ * @param reader the reader to read from
+ * @throws IOException if the I/O fails
+ */
+ public void load(final Reader reader) throws IOException {
+ BufferedReader bufferedReader = new BufferedReader(reader);
+ String line = bufferedReader.readLine();
+ for (; line != null; line = bufferedReader.readLine()) {
+ // Look for lines that resemble:
+ // "key = blah on blah"
+ // "key = bold blah on blah"
+ // "key = blink bold blah on blah"
+ // "key = bold blink blah on blah"
+ // "key = blink blah on blah"
+ if (line.indexOf('=') == -1) {
+ // Invalid line.
+ continue;
+ }
+ String key = line.substring(0, line.indexOf(':')).trim();
+ String text = line.substring(line.indexOf(':') + 1);
+ setColorFromString(key, text);
+ }
+ // All done.
+ bufferedReader.close();
+ }
+
+ /**
+ * Sets to defaults that resemble the Borland IDE colors.
+ */
+ public void setDefaultTheme() {
+ CellAttributes color;
+
+ // TWindow border
+ color = new CellAttributes();
+ color.setForeColor(Color.WHITE);
+ color.setBackColor(Color.BLUE);
+ color.setBold(true);
+ colors.put("twindow.border", color);
+
+ // TWindow background
+ color = new CellAttributes();
+ color.setForeColor(Color.YELLOW);
+ color.setBackColor(Color.BLUE);
+ color.setBold(true);
+ colors.put("twindow.background", color);
+
+ // TWindow border - inactive
+ color = new CellAttributes();
+ color.setForeColor(Color.BLACK);
+ color.setBackColor(Color.BLUE);
+ color.setBold(true);
+ colors.put("twindow.border.inactive", color);
+
+ // TWindow background - inactive
+ color = new CellAttributes();
+ color.setForeColor(Color.YELLOW);
+ color.setBackColor(Color.BLUE);
+ color.setBold(true);
+ colors.put("twindow.background.inactive", color);
+
+ // TWindow border - modal
+ color = new CellAttributes();
+ color.setForeColor(Color.WHITE);
+ color.setBackColor(Color.WHITE);
+ color.setBold(true);
+ colors.put("twindow.border.modal", color);
+
+ // TWindow background - modal
+ color = new CellAttributes();
+ color.setForeColor(Color.BLACK);
+ color.setBackColor(Color.WHITE);
+ color.setBold(false);
+ colors.put("twindow.background.modal", color);
+
+ // TWindow border - modal + inactive
+ color = new CellAttributes();
+ color.setForeColor(Color.BLACK);
+ color.setBackColor(Color.WHITE);
+ color.setBold(true);
+ colors.put("twindow.border.modal.inactive", color);
+
+ // TWindow background - modal + inactive
+ color = new CellAttributes();
+ color.setForeColor(Color.BLACK);
+ color.setBackColor(Color.WHITE);
+ color.setBold(false);
+ colors.put("twindow.background.modal.inactive", color);
+
+ // TWindow border - during window movement - modal
+ color = new CellAttributes();
+ color.setForeColor(Color.GREEN);
+ color.setBackColor(Color.WHITE);
+ color.setBold(true);
+ colors.put("twindow.border.modal.windowmove", color);
+
+ // TWindow border - during window movement
+ color = new CellAttributes();
+ color.setForeColor(Color.GREEN);
+ color.setBackColor(Color.BLUE);
+ color.setBold(true);
+ colors.put("twindow.border.windowmove", color);
+
+ // TWindow background - during window movement
+ color = new CellAttributes();
+ color.setForeColor(Color.YELLOW);
+ color.setBackColor(Color.BLUE);
+ color.setBold(false);
+ colors.put("twindow.background.windowmove", color);
+
+ // TDesktop background
+ color = new CellAttributes();
+ color.setForeColor(Color.BLUE);
+ color.setBackColor(Color.WHITE);
+ color.setBold(false);
+ colors.put("tdesktop.background", color);
+
+ // TButton text
+ color = new CellAttributes();
+ color.setForeColor(Color.BLACK);
+ color.setBackColor(Color.GREEN);
+ color.setBold(false);
+ colors.put("tbutton.inactive", color);
+ color = new CellAttributes();
+ color.setForeColor(Color.CYAN);
+ color.setBackColor(Color.GREEN);
+ color.setBold(true);
+ colors.put("tbutton.active", color);
+ color = new CellAttributes();
+ color.setForeColor(Color.BLACK);
+ color.setBackColor(Color.WHITE);
+ color.setBold(true);
+ colors.put("tbutton.disabled", color);
+ color = new CellAttributes();
+ color.setForeColor(Color.YELLOW);
+ color.setBackColor(Color.GREEN);
+ color.setBold(true);
+ colors.put("tbutton.mnemonic", color);
+ color = new CellAttributes();
+ color.setForeColor(Color.YELLOW);
+ color.setBackColor(Color.GREEN);
+ color.setBold(true);
+ colors.put("tbutton.mnemonic.highlighted", color);
+
+ // TLabel text
+ color = new CellAttributes();
+ color.setForeColor(Color.WHITE);
+ color.setBackColor(Color.BLUE);
+ color.setBold(true);
+ colors.put("tlabel", color);
+ color = new CellAttributes();
+ color.setForeColor(Color.YELLOW);
+ color.setBackColor(Color.BLUE);
+ color.setBold(true);
+ colors.put("tlabel.mnemonic", color);
+
+ // TText text
+ color = new CellAttributes();
+ color.setForeColor(Color.WHITE);
+ color.setBackColor(Color.BLUE);
+ color.setBold(false);
+ colors.put("ttext", color);
+
+ // TField text
+ color = new CellAttributes();
+ color.setForeColor(Color.BLACK);
+ color.setBackColor(Color.WHITE);
+ color.setBold(false);
+ colors.put("tfield.inactive", color);
+ color = new CellAttributes();
+ color.setForeColor(Color.BLACK);
+ color.setBackColor(Color.CYAN);
+ color.setBold(false);
+ colors.put("tfield.active", color);
+
+ // TCheckBox
+ color = new CellAttributes();
+ color.setForeColor(Color.WHITE);
+ color.setBackColor(Color.BLUE);
+ color.setBold(false);
+ colors.put("tcheckbox.inactive", color);
+ color = new CellAttributes();
+ color.setForeColor(Color.YELLOW);
+ color.setBackColor(Color.BLACK);
+ color.setBold(true);
+ colors.put("tcheckbox.active", color);
+ color = new CellAttributes();
+ color.setForeColor(Color.YELLOW);
+ color.setBackColor(Color.BLUE);
+ color.setBold(true);
+ colors.put("tcheckbox.mnemonic", color);
+ color = new CellAttributes();
+ color.setForeColor(Color.RED);
+ color.setBackColor(Color.BLACK);
+ color.setBold(true);
+ colors.put("tcheckbox.mnemonic.highlighted", color);
+
+ // TComboBox
+ color = new CellAttributes();
+ color.setForeColor(Color.BLACK);
+ color.setBackColor(Color.WHITE);
+ color.setBold(false);
+ colors.put("tcombobox.inactive", color);
+ color = new CellAttributes();
+ color.setForeColor(Color.BLUE);
+ color.setBackColor(Color.CYAN);
+ color.setBold(false);
+ colors.put("tcombobox.active", color);
+
+ // TSpinner
+ color = new CellAttributes();
+ color.setForeColor(Color.BLACK);
+ color.setBackColor(Color.WHITE);
+ color.setBold(false);
+ colors.put("tspinner.inactive", color);
+ color = new CellAttributes();
+ color.setForeColor(Color.BLUE);
+ color.setBackColor(Color.CYAN);
+ color.setBold(false);
+ colors.put("tspinner.active", color);
+
+ // TCalendar
+ color = new CellAttributes();
+ color.setForeColor(Color.WHITE);
+ color.setBackColor(Color.BLUE);
+ color.setBold(false);
+ colors.put("tcalendar.background", color);
+ color = new CellAttributes();
+ color.setForeColor(Color.WHITE);
+ color.setBackColor(Color.BLUE);
+ color.setBold(false);
+ colors.put("tcalendar.day", color);
+ color = new CellAttributes();
+ color.setForeColor(Color.RED);
+ color.setBackColor(Color.WHITE);
+ color.setBold(false);
+ colors.put("tcalendar.day.selected", color);
+ color = new CellAttributes();
+ color.setForeColor(Color.BLUE);
+ color.setBackColor(Color.CYAN);
+ color.setBold(false);
+ colors.put("tcalendar.arrow", color);
+ color = new CellAttributes();
+ color.setForeColor(Color.WHITE);
+ color.setBackColor(Color.BLUE);
+ color.setBold(true);
+ colors.put("tcalendar.title", color);
+
+ // TRadioButton
+ color = new CellAttributes();
+ color.setForeColor(Color.WHITE);
+ color.setBackColor(Color.BLUE);
+ color.setBold(false);
+ colors.put("tradiobutton.inactive", color);
+ color = new CellAttributes();
+ color.setForeColor(Color.YELLOW);
+ color.setBackColor(Color.BLACK);
+ color.setBold(true);
+ colors.put("tradiobutton.active", color);
+ color = new CellAttributes();
+ color.setForeColor(Color.YELLOW);
+ color.setBackColor(Color.BLUE);
+ color.setBold(true);
+ colors.put("tradiobutton.mnemonic", color);
+ color = new CellAttributes();
+ color.setForeColor(Color.RED);
+ color.setBackColor(Color.BLACK);
+ color.setBold(true);
+ colors.put("tradiobutton.mnemonic.highlighted", color);
+
+ // TRadioGroup
+ color = new CellAttributes();
+ color.setForeColor(Color.WHITE);
+ color.setBackColor(Color.BLUE);
+ color.setBold(false);
+ colors.put("tradiogroup.inactive", color);
+ color = new CellAttributes();
+ color.setForeColor(Color.YELLOW);
+ color.setBackColor(Color.BLUE);
+ color.setBold(true);
+ colors.put("tradiogroup.active", color);
+
+ // TMenu
+ color = new CellAttributes();
+ color.setForeColor(Color.BLACK);
+ color.setBackColor(Color.WHITE);
+ color.setBold(false);
+ colors.put("tmenu", color);
+ color = new CellAttributes();
+ color.setForeColor(Color.BLACK);
+ color.setBackColor(Color.GREEN);
+ color.setBold(false);
+ colors.put("tmenu.highlighted", color);
+ color = new CellAttributes();
+ color.setForeColor(Color.RED);
+ color.setBackColor(Color.WHITE);
+ color.setBold(false);
+ colors.put("tmenu.mnemonic", color);
+ color = new CellAttributes();
+ color.setForeColor(Color.RED);
+ color.setBackColor(Color.GREEN);
+ color.setBold(false);
+ colors.put("tmenu.mnemonic.highlighted", color);
+ color = new CellAttributes();
+ color.setForeColor(Color.BLACK);
+ color.setBackColor(Color.WHITE);
+ color.setBold(true);
+ colors.put("tmenu.disabled", color);
+
+ // TProgressBar
+ color = new CellAttributes();
+ color.setForeColor(Color.BLUE);
+ color.setBackColor(Color.BLUE);
+ color.setBold(true);
+ colors.put("tprogressbar.complete", color);
+ color = new CellAttributes();
+ color.setForeColor(Color.WHITE);
+ color.setBackColor(Color.BLUE);
+ color.setBold(false);
+ colors.put("tprogressbar.incomplete", color);
+
+ // THScroller / TVScroller
+ color = new CellAttributes();
+ color.setForeColor(Color.CYAN);
+ color.setBackColor(Color.BLUE);
+ color.setBold(false);
+ colors.put("tscroller.bar", color);
+ color = new CellAttributes();
+ color.setForeColor(Color.BLUE);
+ color.setBackColor(Color.CYAN);
+ color.setBold(false);
+ colors.put("tscroller.arrows", color);
+
+ // TTreeView
+ color = new CellAttributes();
+ color.setForeColor(Color.WHITE);
+ color.setBackColor(Color.BLUE);
+ color.setBold(false);
+ colors.put("ttreeview", color);
+ color = new CellAttributes();
+ color.setForeColor(Color.GREEN);
+ color.setBackColor(Color.BLUE);
+ color.setBold(true);
+ colors.put("ttreeview.expandbutton", color);
+ color = new CellAttributes();
+ color.setForeColor(Color.BLACK);
+ color.setBackColor(Color.CYAN);
+ color.setBold(false);
+ colors.put("ttreeview.selected", color);
+ color = new CellAttributes();
+ color.setForeColor(Color.RED);
+ color.setBackColor(Color.BLUE);
+ color.setBold(false);
+ colors.put("ttreeview.unreadable", color);
+ color = new CellAttributes();
+ // color.setForeColor(Color.BLACK);
+ // color.setBackColor(Color.BLUE);
+ // color.setBold(true);
+ color.setForeColor(Color.WHITE);
+ color.setBackColor(Color.BLUE);
+ color.setBold(false);
+ colors.put("ttreeview.inactive", color);
+ color = new CellAttributes();
+ color.setForeColor(Color.BLACK);
+ color.setBackColor(Color.WHITE);
+ color.setBold(false);
+ colors.put("ttreeview.selected.inactive", color);
+
+ // TList
+ color = new CellAttributes();
+ color.setForeColor(Color.WHITE);
+ color.setBackColor(Color.BLUE);
+ color.setBold(false);
+ colors.put("tlist", color);
+ color = new CellAttributes();
+ color.setForeColor(Color.BLACK);
+ color.setBackColor(Color.CYAN);
+ color.setBold(false);
+ colors.put("tlist.selected", color);
+ color = new CellAttributes();
+ color.setForeColor(Color.BLACK);
+ color.setBackColor(Color.CYAN);
+ color.setBold(false);
+ colors.put("tlist.unreadable", color);
+ color = new CellAttributes();
+ // color.setForeColor(Color.BLACK);
+ // color.setBackColor(Color.BLUE);
+ // color.setBold(true);
+ color.setForeColor(Color.WHITE);
+ color.setBackColor(Color.BLUE);
+ color.setBold(false);
+ colors.put("tlist.inactive", color);
+ color = new CellAttributes();
+ color.setForeColor(Color.BLACK);
+ color.setBackColor(Color.WHITE);
+ color.setBold(false);
+ colors.put("tlist.selected.inactive", color);
+
+ // TStatusBar
+ color = new CellAttributes();
+ color.setForeColor(Color.BLACK);
+ color.setBackColor(Color.WHITE);
+ color.setBold(false);
+ colors.put("tstatusbar.text", color);
+ color = new CellAttributes();
+ color.setForeColor(Color.RED);
+ color.setBackColor(Color.WHITE);
+ color.setBold(false);
+ colors.put("tstatusbar.button", color);
+ color = new CellAttributes();
+ color.setForeColor(Color.WHITE);
+ color.setBackColor(Color.BLUE);
+ color.setBold(false);
+ colors.put("tstatusbar.selected", color);
+
+ // TEditor
+ color = new CellAttributes();
+ color.setForeColor(Color.WHITE);
+ color.setBackColor(Color.BLUE);
+ color.setBold(false);
+ colors.put("teditor", color);
+
+ // TTable
+ color = new CellAttributes();
+ color.setForeColor(Color.WHITE);
+ color.setBackColor(Color.BLUE);
+ color.setBold(false);
+ colors.put("ttable.inactive", color);
+ color = new CellAttributes();
+ color.setForeColor(Color.BLACK);
+ color.setBackColor(Color.CYAN);
+ color.setBold(false);
+ colors.put("ttable.active", color);
+ color = new CellAttributes();
+ color.setForeColor(Color.YELLOW);
+ color.setBackColor(Color.CYAN);
+ color.setBold(true);
+ colors.put("ttable.selected", color);
+ color = new CellAttributes();
+ color.setForeColor(Color.BLACK);
+ color.setBackColor(Color.WHITE);
+ color.setBold(false);
+ colors.put("ttable.label", color);
+ color = new CellAttributes();
+ color.setForeColor(Color.BLUE);
+ color.setBackColor(Color.WHITE);
+ color.setBold(false);
+ colors.put("ttable.label.selected", color);
+ color = new CellAttributes();
+ color.setForeColor(Color.WHITE);
+ color.setBackColor(Color.BLUE);
+ color.setBold(false);
+ colors.put("ttable.border", color);
+
+ // TSplitPane
+ color = new CellAttributes();
+ color.setForeColor(Color.WHITE);
+ color.setBackColor(Color.BLUE);
+ color.setBold(false);
+ colors.put("tsplitpane", color);
+
+ }
+
+ /**
+ * Make human-readable description of this Cell.
+ *
+ * @return displayable String
+ */
+ @Override
+ public String toString() {
+ return colors.toString();
+ }
+
+ }
--- /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;
+
+ /**
+ * This class contains a collection of special characters used by the
+ * windowing system and the mappings from CP437 to Unicode.
+ */
+ public final class GraphicsChars {
+
+ // ------------------------------------------------------------------------
+ // Constants --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The CP437 to Unicode translation map.
+ */
+ public static final char [] CP437 = {
+ '\u2007', '\u263A', '\u263B', '\u2665',
+ '\u2666', '\u2663', '\u2660', '\u2022',
+ '\u25D8', '\u25CB', '\u25D9', '\u2642',
+ '\u2640', '\u266A', '\u266B', '\u263C',
+ // Terminus has 25B6 and 25C0 here, which I believe are better
+ // Unicode equivalents anyway.
+ // '\u25BA', '\u25C4', '\u2195', '\u203C',
+ '\u25B6', '\u25C0', '\u2195', '\u203C',
+ '\u00B6', '\u00A7', '\u25AC', '\u21A8',
+ '\u2191', '\u2193', '\u2192', '\u2190',
+ '\u221F', '\u2194', '\u25B2', '\u25BC',
+ '\u0020', '\u0021', '\"', '\u0023',
+ '\u0024', '\u0025', '\u0026', '\'',
+ '\u0028', '\u0029', '\u002a', '\u002b',
+ '\u002c', '\u002d', '\u002e', '\u002f',
+ '\u0030', '\u0031', '\u0032', '\u0033',
+ '\u0034', '\u0035', '\u0036', '\u0037',
+ '\u0038', '\u0039', '\u003a', '\u003b',
+ '\u003c', '\u003d', '\u003e', '\u003f',
+ '\u0040', '\u0041', '\u0042', '\u0043',
+ '\u0044', '\u0045', '\u0046', '\u0047',
+ '\u0048', '\u0049', '\u004a', '\u004b',
+ '\u004c', '\u004d', '\u004e', '\u004f',
+ '\u0050', '\u0051', '\u0052', '\u0053',
+ '\u0054', '\u0055', '\u0056', '\u0057',
+ '\u0058', '\u0059', '\u005a', '\u005b',
+ '\\', '\u005d', '\u005e', '\u005f',
+ '\u0060', '\u0061', '\u0062', '\u0063',
+ '\u0064', '\u0065', '\u0066', '\u0067',
+ '\u0068', '\u0069', '\u006a', '\u006b',
+ '\u006c', '\u006d', '\u006e', '\u006f',
+ '\u0070', '\u0071', '\u0072', '\u0073',
+ '\u0074', '\u0075', '\u0076', '\u0077',
+ '\u0078', '\u0079', '\u007a', '\u007b',
+ '\u007c', '\u007d', '\u007e', '\u2302',
+ '\u00c7', '\u00fc', '\u00e9', '\u00e2',
+ '\u00e4', '\u00e0', '\u00e5', '\u00e7',
+ '\u00ea', '\u00eb', '\u00e8', '\u00ef',
+ '\u00ee', '\u00ec', '\u00c4', '\u00c5',
+ '\u00c9', '\u00e6', '\u00c6', '\u00f4',
+ '\u00f6', '\u00f2', '\u00fb', '\u00f9',
+ '\u00ff', '\u00d6', '\u00dc', '\u00a2',
+ '\u00a3', '\u00a5', '\u20a7', '\u0192',
+ '\u00e1', '\u00ed', '\u00f3', '\u00fa',
+ '\u00f1', '\u00d1', '\u00aa', '\u00ba',
+ '\u00bf', '\u2310', '\u00ac', '\u00bd',
+ '\u00bc', '\u00a1', '\u00ab', '\u00bb',
+ '\u2591', '\u2592', '\u2593', '\u2502',
+ '\u2524', '\u2561', '\u2562', '\u2556',
+ '\u2555', '\u2563', '\u2551', '\u2557',
+ '\u255d', '\u255c', '\u255b', '\u2510',
+ '\u2514', '\u2534', '\u252c', '\u251c',
+ '\u2500', '\u253c', '\u255e', '\u255f',
+ '\u255a', '\u2554', '\u2569', '\u2566',
+ '\u2560', '\u2550', '\u256c', '\u2567',
+ '\u2568', '\u2564', '\u2565', '\u2559',
+ '\u2558', '\u2552', '\u2553', '\u256b',
+ '\u256a', '\u2518', '\u250c', '\u2588',
+ '\u2584', '\u258c', '\u2590', '\u2580',
+ '\u03b1', '\u00df', '\u0393', '\u03c0',
+ '\u03a3', '\u03c3', '\u00b5', '\u03c4',
+ '\u03a6', '\u0398', '\u03a9', '\u03b4',
+ '\u221e', '\u03c6', '\u03b5', '\u2229',
+ '\u2261', '\u00b1', '\u2265', '\u2264',
+ '\u2320', '\u2321', '\u00f7', '\u2248',
+ '\u00b0', '\u2219', '\u00b7', '\u221a',
+ '\u207f', '\u00b2', '\u25a0', '\u00a0'
+ };
+
+ public static final char HATCH = CP437[0xB0];
+ public static final char DOUBLE_BAR = CP437[0xCD];
+ public static final char BOX = CP437[0xFE];
+ public static final char CHECK = CP437[0xFB];
+ public static final char TRIPLET = CP437[0xF0];
+ public static final char OMEGA = CP437[0xEA];
+ public static final char PI = CP437[0xE3];
+ public static final char UPARROW = CP437[0x18];
+ public static final char DOWNARROW = CP437[0x19];
+ public static final char RIGHTARROW = CP437[0x1A];
+ public static final char LEFTARROW = CP437[0x1B];
+ public static final char SINGLE_BAR = CP437[0xC4];
+ public static final char BACK_ARROWHEAD = CP437[0x11];
+ public static final char LRCORNER = CP437[0xD9];
+ public static final char URCORNER = CP437[0xBF];
+ public static final char LLCORNER = CP437[0xC0];
+ public static final char ULCORNER = CP437[0xDA];
+ public static final char DEGREE = CP437[0xF8];
+ public static final char PLUSMINUS = CP437[0xF1];
+ public static final char WINDOW_TOP = CP437[0xCD];
+ public static final char WINDOW_LEFT_TOP = CP437[0xD5];
+ public static final char WINDOW_RIGHT_TOP = CP437[0xB8];
+ public static final char WINDOW_SIDE = CP437[0xB3];
+ public static final char WINDOW_LEFT_BOTTOM = CP437[0xD4];
+ public static final char WINDOW_RIGHT_BOTTOM = CP437[0xBE];
+ public static final char WINDOW_LEFT_TEE = CP437[0xC6];
+ public static final char WINDOW_RIGHT_TEE = CP437[0xB5];
+ public static final char WINDOW_SIDE_DOUBLE = CP437[0xBA];
+ public static final char WINDOW_LEFT_TOP_DOUBLE = CP437[0xC9];
+ public static final char WINDOW_RIGHT_TOP_DOUBLE = CP437[0xBB];
+ public static final char WINDOW_LEFT_BOTTOM_DOUBLE = CP437[0xC8];
+ public static final char WINDOW_RIGHT_BOTTOM_DOUBLE = CP437[0xBC];
+ public static final char VERTICAL_BAR = CP437[0xB3];
+ public static final char OCTOSTAR = CP437[0x0F];
+ public static final char DOWNARROWLEFT = CP437[0xDD];
+ public static final char DOWNARROWRIGHT = CP437[0xDE];
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Private constructor prevents accidental creation of this class.
+ */
+ private GraphicsChars() {
+ }
+
+ }
--- /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;
+
+ /**
+ * MnemonicString is used to render a string like "&File" into a
+ * highlighted 'F' and the rest of 'ile'. To insert a literal '&', use
+ * two '&&' characters, e.g. "&File && Stuff" would be
+ * "File & Stuff" with the first 'F' highlighted.
+ */
+ public class MnemonicString {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Keyboard shortcut to activate this item.
+ */
+ private int shortcut;
+
+ /**
+ * Location of the highlighted character.
+ */
+ private int shortcutIdx = -1;
+
+ /**
+ * Screen location of the highlighted character (number of text cells
+ * required to display from the beginning to shortcutIdx).
+ */
+ private int screenShortcutIdx = -1;
+
+ /**
+ * The raw (uncolored) string.
+ */
+ private String rawLabel;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param label widget label or title. Label must contain a keyboard
+ * shortcut, denoted by prefixing a letter with "&", e.g. "&File"
+ */
+ public MnemonicString(final String label) {
+
+ // Setup the menu shortcut
+ StringBuilder newLabel = new StringBuilder();
+ boolean foundAmp = false;
+ boolean foundShortcut = false;
+ int scanShortcutIdx = 0;
+ int scanScreenShortcutIdx = 0;
+ for (int i = 0; i < label.length();) {
+ int c = label.codePointAt(i);
+ i += Character.charCount(c);
+
+ if (c == '&') {
+ if (foundAmp) {
+ newLabel.append('&');
+ scanShortcutIdx++;
+ scanScreenShortcutIdx++;
+ } else {
+ foundAmp = true;
+ }
+ } else {
+ newLabel.append(Character.toChars(c));
+ if (foundAmp) {
+ if (!foundShortcut) {
+ shortcut = c;
+ foundAmp = false;
+ foundShortcut = true;
+ shortcutIdx = scanShortcutIdx;
+ screenShortcutIdx = scanScreenShortcutIdx;
+ }
+ } else {
+ scanShortcutIdx++;
+ scanScreenShortcutIdx += StringUtils.width(c);
+ }
+ }
+ }
+ this.rawLabel = newLabel.toString();
+ }
+
+ // ------------------------------------------------------------------------
+ // MnemonicString ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get the keyboard shortcut character.
+ *
+ * @return the highlighted character
+ */
+ public int getShortcut() {
+ return shortcut;
+ }
+
+ /**
+ * Get location of the highlighted character.
+ *
+ * @return location of the highlighted character
+ */
+ public int getShortcutIdx() {
+ return shortcutIdx;
+ }
+
+ /**
+ * Get the screen location of the highlighted character.
+ *
+ * @return the number of text cells required to display from the
+ * beginning of the label to shortcutIdx
+ */
+ public int getScreenShortcutIdx() {
+ return screenShortcutIdx;
+ }
+
+ /**
+ * Get the raw (uncolored) string.
+ *
+ * @return the raw (uncolored) string
+ */
+ public String getRawLabel() {
+ return rawLabel;
+ }
+
+ }
--- /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.util.List;
+ import java.util.ArrayList;
+
+ /**
+ * StringUtils contains methods to:
+ *
+ * - Convert one or more long lines of strings into justified text
+ * paragraphs.
+ *
+ * - Unescape C0 control codes.
+ *
+ * - Read/write a line of RFC4180 comma-separated values strings to/from a
+ * list of strings.
+ */
+ public class StringUtils {
+
+ /**
+ * Left-justify a string into a list of lines.
+ *
+ * @param str the string
+ * @param n the maximum number of characters in a line
+ * @return the list of lines
+ */
+ public static List<String> left(final String str, final int n) {
+ List<String> result = new ArrayList<String>();
+
+ /*
+ * General procedure:
+ *
+ * 1. Split on '\n' into paragraphs.
+ *
+ * 2. Scan each line, noting the position of the last
+ * beginning-of-a-word.
+ *
+ * 3. Chop at the last #2 if the next beginning-of-a-word exceeds
+ * n.
+ *
+ * 4. Return the lines.
+ */
+
+ String [] rawLines = str.split("\n");
+ for (int i = 0; i < rawLines.length; i++) {
+ StringBuilder line = new StringBuilder();
+ StringBuilder word = new StringBuilder();
+ boolean inWord = false;
+ for (int j = 0; j < rawLines[i].length(); j++) {
+ char ch = rawLines[i].charAt(j);
+ if ((ch == ' ') || (ch == '\t')) {
+ if (inWord == true) {
+ // We have just transitioned from a word to
+ // whitespace. See if we have enough space to add
+ // the word to the line.
+ if (width(word.toString()) + width(line.toString()) > n) {
+ // This word will exceed the line length. Wrap
+ // at it instead.
+ result.add(line.toString());
+ line = new StringBuilder();
+ }
+ if ((word.toString().startsWith(" "))
+ && (width(line.toString()) == 0)
+ ) {
+ line.append(word.substring(1));
+ } else {
+ line.append(word);
+ }
+ word = new StringBuilder();
+ word.append(ch);
+ inWord = false;
+ } else {
+ // We are in the whitespace before another word. Do
+ // nothing.
+ }
+ } else {
+ if (inWord == true) {
+ // We are appending to a word.
+ word.append(ch);
+ } else {
+ // We have transitioned from whitespace to a word.
+ word.append(ch);
+ inWord = true;
+ }
+ }
+ } // for (int j = 0; j < rawLines[i].length(); j++)
+
+ if (width(word.toString()) + width(line.toString()) > n) {
+ // This word will exceed the line length. Wrap at it
+ // instead.
+ result.add(line.toString());
+ line = new StringBuilder();
+ }
+ if ((word.toString().startsWith(" "))
+ && (width(line.toString()) == 0)
+ ) {
+ line.append(word.substring(1));
+ } else {
+ line.append(word);
+ }
+ result.add(line.toString());
+ } // for (int i = 0; i < rawLines.length; i++) {
+
+ return result;
+ }
+
+ /**
+ * Right-justify a string into a list of lines.
+ *
+ * @param str the string
+ * @param n the maximum number of characters in a line
+ * @return the list of lines
+ */
+ public static List<String> right(final String str, final int n) {
+ List<String> result = new ArrayList<String>();
+
+ /*
+ * Same as left(), but preceed each line with spaces to make it n
+ * chars long.
+ */
+ List<String> lines = left(str, n);
+ for (String line: lines) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < n - width(line); i++) {
+ sb.append(' ');
+ }
+ sb.append(line);
+ result.add(sb.toString());
+ }
+
+ return result;
+ }
+
+ /**
+ * Center a string into a list of lines.
+ *
+ * @param str the string
+ * @param n the maximum number of characters in a line
+ * @return the list of lines
+ */
+ public static List<String> center(final String str, final int n) {
+ List<String> result = new ArrayList<String>();
+
+ /*
+ * Same as left(), but preceed/succeed each line with spaces to make
+ * it n chars long.
+ */
+ List<String> lines = left(str, n);
+ for (String line: lines) {
+ StringBuilder sb = new StringBuilder();
+ int l = (n - width(line)) / 2;
+ int r = n - width(line) - l;
+ for (int i = 0; i < l; i++) {
+ sb.append(' ');
+ }
+ sb.append(line);
+ for (int i = 0; i < r; i++) {
+ sb.append(' ');
+ }
+ result.add(sb.toString());
+ }
+
+ return result;
+ }
+
+ /**
+ * Fully-justify a string into a list of lines.
+ *
+ * @param str the string
+ * @param n the maximum number of characters in a line
+ * @return the list of lines
+ */
+ public static List<String> full(final String str, final int n) {
+ List<String> result = new ArrayList<String>();
+
+ /*
+ * Same as left(), but insert spaces between words to make each line
+ * n chars long. The "algorithm" here is pretty dumb: it performs a
+ * split on space and then re-inserts multiples of n between words.
+ */
+ List<String> lines = left(str, n);
+ for (int lineI = 0; lineI < lines.size() - 1; lineI++) {
+ String line = lines.get(lineI);
+ String [] words = line.split(" ");
+ if (words.length > 1) {
+ int charCount = 0;
+ for (int i = 0; i < words.length; i++) {
+ charCount += words[i].length();
+ }
+ int spaceCount = n - charCount;
+ int q = spaceCount / (words.length - 1);
+ int r = spaceCount % (words.length - 1);
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < words.length - 1; i++) {
+ sb.append(words[i]);
+ for (int j = 0; j < q; j++) {
+ sb.append(' ');
+ }
+ if (r > 0) {
+ sb.append(' ');
+ r--;
+ }
+ }
+ for (int j = 0; j < r; j++) {
+ sb.append(' ');
+ }
+ sb.append(words[words.length - 1]);
+ result.add(sb.toString());
+ } else {
+ result.add(line);
+ }
+ }
+ if (lines.size() > 0) {
+ result.add(lines.get(lines.size() - 1));
+ }
+
+ return result;
+ }
+
+ /**
+ * Convert raw strings into escaped strings that be splatted on the
+ * screen.
+ *
+ * @param str the string
+ * @return a string that can be passed into Screen.putStringXY()
+ */
+ public static String unescape(final String str) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < str.length(); i++) {
+ char ch = str.charAt(i);
+ if ((ch < 0x20) || (ch == 0x7F)) {
+ switch (ch) {
+ case '\b':
+ sb.append("\\b");
+ continue;
+ case '\f':
+ sb.append("\\f");
+ continue;
+ case '\n':
+ sb.append("\\n");
+ continue;
+ case '\r':
+ sb.append("\\r");
+ continue;
+ case '\t':
+ sb.append("\\t");
+ continue;
+ case 0x7f:
+ sb.append("^?");
+ continue;
+ default:
+ sb.append(' ');
+ continue;
+ }
+ }
+ sb.append(ch);
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Read a line of RFC4180 comma-separated values (CSV) into a list of
+ * strings.
+ *
+ * @param line the CSV line, with or without without line terminators
+ * @return the list of strings
+ */
+ public static List<String> fromCsv(final String line) {
+ List<String> result = new ArrayList<String>();
+
+ StringBuilder str = new StringBuilder();
+ boolean quoted = false;
+ boolean fieldQuoted = false;
+
+ for (int i = 0; i < line.length(); i++) {
+ char ch = line.charAt(i);
+
+ /*
+ System.err.println("ch '" + ch + "' str '" + str + "' " +
+ " fieldQuoted " + fieldQuoted + " quoted " + quoted);
+ */
+
+ if (ch == ',') {
+ if (fieldQuoted && quoted) {
+ // Terminating a quoted field.
+ result.add(str.toString());
+ str = new StringBuilder();
+ quoted = false;
+ fieldQuoted = false;
+ } else if (fieldQuoted) {
+ // Still waiting to see the terminating quote for this
+ // field.
+ str.append(ch);
+ } else if (quoted) {
+ // An unmatched double-quote and comma. This should be
+ // an invalid sequence. We will treat it as a quote
+ // terminating the field.
+ str.append('\"');
+ result.add(str.toString());
+ str = new StringBuilder();
+ quoted = false;
+ fieldQuoted = false;
+ } else {
+ // A field separator.
+ result.add(str.toString());
+ str = new StringBuilder();
+ quoted = false;
+ fieldQuoted = false;
+ }
+ continue;
+ }
+
+ if (ch == '\"') {
+ if ((str.length() == 0) && (!fieldQuoted)) {
+ // The opening quote to a quoted field.
+ fieldQuoted = true;
+ } else if (quoted) {
+ // This is a double-quote.
+ str.append('\"');
+ quoted = false;
+ } else {
+ // This is the beginning of a quote.
+ quoted = true;
+ }
+ continue;
+ }
+
+ // Normal character, pass it on.
+ str.append(ch);
+ }
+
+ // Include the final field.
+ result.add(str.toString());
+
+ return result;
+ }
+
+ /**
+ * Write a list of strings to on line of RFC4180 comma-separated values
+ * (CSV).
+ *
+ * @param list the list of strings
+ * @return the CSV line, without any line terminators
+ */
+ public static String toCsv(final List<String> list) {
+ StringBuilder result = new StringBuilder();
+ int i = 0;
+ for (String str: list) {
+
+ if (!str.contains("\"") && !str.contains(",")) {
+ // Just append the string with a comma.
+ result.append(str);
+ } else if (!str.contains("\"") && str.contains(",")) {
+ // Contains commas, but no quotes. Just double-quote it.
+ result.append("\"");
+ result.append(str);
+ result.append("\"");
+ } else if (str.contains("\"")) {
+ // Contains quotes and maybe commas. Double-quote it and
+ // replace quotes inside.
+ result.append("\"");
+ for (int j = 0; j < str.length(); j++) {
+ char ch = str.charAt(j);
+ result.append(ch);
+ if (ch == '\"') {
+ result.append("\"");
+ }
+ }
+ result.append("\"");
+ }
+
+ if (i < list.size() - 1) {
+ result.append(",");
+ }
+ i++;
+ }
+ return result.toString();
+ }
+
+ /**
+ * Determine display width of a Unicode code point.
+ *
+ * @param ch the code point, can be char
+ * @return the number of text cell columns required to display this code
+ * point, one of 0, 1, or 2
+ */
+ public static int width(final int ch) {
+ /*
+ * This routine is a modified version of mk_wcwidth() available
+ * at: http://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c
+ *
+ * The combining characters list has been omitted from this
+ * implementation. Hopefully no users will be impacted.
+ */
+
+ // 8-bit control characters: width 0
+ if (ch == 0) {
+ return 0;
+ }
+ if ((ch < 32) || ((ch >= 0x7f) && (ch < 0xa0))) {
+ return 0;
+ }
+
+ // All others: either 1 or 2
+ if ((ch >= 0x1100)
+ && ((ch <= 0x115f)
+ // Hangul Jamo init. consonants
+ || (ch == 0x2329)
+ || (ch == 0x232a)
+ // CJK ... Yi
+ || ((ch >= 0x2e80) && (ch <= 0xa4cf) && (ch != 0x303f))
+ // Hangul Syllables
+ || ((ch >= 0xac00) && (ch <= 0xd7a3))
+ // CJK Compatibility Ideographs
+ || ((ch >= 0xf900) && (ch <= 0xfaff))
+ // Vertical forms
+ || ((ch >= 0xfe10) && (ch <= 0xfe19))
+ // CJK Compatibility Forms
+ || ((ch >= 0xfe30) && (ch <= 0xfe6f))
+ // Fullwidth Forms
+ || ((ch >= 0xff00) && (ch <= 0xff60))
+ || ((ch >= 0xffe0) && (ch <= 0xffe6))
+ || ((ch >= 0x20000) && (ch <= 0x2fffd))
+ || ((ch >= 0x30000) && (ch <= 0x3fffd))
+ // emoji
+ || ((ch >= 0x1f004) && (ch <= 0x1fffd))
+ )
+ ) {
+ return 2;
+ }
+ return 1;
+ }
+
+ /**
+ * Determine display width of a string. This ASSUMES that no characters
+ * are combining. Hopefully no users will be impacted.
+ *
+ * @param str the string
+ * @return the number of text cell columns required to display this string
+ */
+ public static int width(final String str) {
+ int n = 0;
+ for (int i = 0; i < str.length();) {
+ int ch = str.codePointAt(i);
+ n += width(ch);
+ i += Character.charCount(ch);
+ }
+ return n;
+ }
+
+ /**
+ * Check if character is in the CJK range.
+ *
+ * @param ch character to check
+ * @return true if this character is in the CJK range
+ */
+ public static boolean isCjk(final int ch) {
+ return ((ch >= 0x2e80) && (ch <= 0x9fff));
+ }
+
+ /**
+ * Check if character is in the emoji range.
+ *
+ * @param ch character to check
+ * @return true if this character is in the emoji range
+ */
+ public static boolean isEmoji(final int ch) {
+ return ((ch >= 0x1f004) && (ch <= 0x1fffd));
+ }
+
+ }
--- /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
+ */
+
+ /**
+ * Low-level data objects and utility functions that don't warrant their own
+ * separate package.
+ */
+ package jexer.bits;
--- /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 jexer.TApplication;
+
+ /**
+ * This class is the main driver for a simple demonstration of Jexer's
+ * capabilities.
+ */
+ public class Demo1 {
+
+ /**
+ * Main entry point.
+ *
+ * @param args Command line arguments
+ */
+ public static void main(final String [] args) {
+ try {
+ // Swing is the default backend on Windows unless explicitly
+ // overridden by jexer.Swing.
+ TApplication.BackendType backendType = TApplication.BackendType.XTERM;
+ if (System.getProperty("os.name").startsWith("Windows")) {
+ backendType = TApplication.BackendType.SWING;
+ }
+ if (System.getProperty("os.name").startsWith("Mac")) {
+ backendType = TApplication.BackendType.SWING;
+ }
+ if (System.getProperty("jexer.Swing") != null) {
+ if (System.getProperty("jexer.Swing", "false").equals("true")) {
+ backendType = TApplication.BackendType.SWING;
+ } else {
+ backendType = TApplication.BackendType.XTERM;
+ }
+ }
+ DemoApplication app = new DemoApplication(backendType);
+ (new Thread(app)).start();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ }
--- /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.net.TelnetServerSocket;
+
+ /**
+ * This class is the main driver for a simple demonstration of Jexer's
+ * capabilities. Rather than run locally, it serves a Jexer UI over a TCP
+ * port.
+ */
+ public class Demo2 {
+
+ /**
+ * Translated strings.
+ */
+ private static final ResourceBundle i18n = ResourceBundle.getBundle(Demo2.class.getName());
+
+ /**
+ * Main entry point.
+ *
+ * @param args Command line arguments
+ */
+ public static void main(final String [] args) {
+ ServerSocket server = null;
+ try {
+ if (args.length == 0) {
+ System.err.println(i18n.getString("usageString"));
+ return;
+ }
+
+ int port = Integer.parseInt(args[0]);
+ server = new TelnetServerSocket(port);
+ while (true) {
+ Socket socket = server.accept();
+ System.out.println(MessageFormat.
+ format(i18n.getString("newConnection"), socket));
+ DemoApplication app = new DemoApplication(socket.getInputStream(),
+ socket.getOutputStream());
+ (new Thread(app)).start();
+ 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()));
+ }
+ } 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.Demo2 port
+ newConnection=New connection: {0}
+ username=\ \ \ username: {0}
+ language=\ \ \ language: {0}
+ terminal=\ \ \ terminal: {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.demos;
+
+ import java.io.*;
+
+ /**
+ * This class is the main driver for a simple demonstration of Jexer's
+ * capabilities. This one passes separate Reader/Writer to TApplication,
+ * which will behave quite badly due to System.in/out not being in raw mode.
+ */
+ public class Demo3 {
+
+ /**
+ * Main entry point.
+ *
+ * @param args Command line arguments
+ */
+ public static void main(final String [] args) {
+ try {
+ DemoApplication app = new DemoApplication(System.in,
+ new InputStreamReader(System.in, "UTF-8"),
+ new PrintWriter(new OutputStreamWriter(System.out, "UTF-8")),
+ true);
+ (new Thread(app)).start();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ }
--- /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 jexer.*;
+
+ /**
+ * This class is the main driver for a simple demonstration of Jexer's
+ * capabilities. This one shows TDesktop and TWindow API details.
+ */
+ public class Demo4 {
+
+ /**
+ * Main entry point.
+ *
+ * @param args Command line arguments
+ */
+ public static void main(final String [] args) {
+ try {
+ // Swing is the default backend on Windows unless explicitly
+ // overridden by jexer.Swing.
+ TApplication.BackendType backendType = TApplication.BackendType.XTERM;
+ if (System.getProperty("os.name").startsWith("Windows")) {
+ backendType = TApplication.BackendType.SWING;
+ }
+ if (System.getProperty("os.name").startsWith("Mac")) {
+ backendType = TApplication.BackendType.SWING;
+ }
+ if (System.getProperty("jexer.Swing") != null) {
+ if (System.getProperty("jexer.Swing", "false").equals("true")) {
+ backendType = TApplication.BackendType.SWING;
+ } else {
+ backendType = TApplication.BackendType.XTERM;
+ }
+ }
+ DesktopDemoApplication app = new DesktopDemoApplication(backendType);
+ (new Thread(app)).start();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ }
--- /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.awt.Font;
+ import java.awt.event.WindowEvent;
+ import java.awt.event.WindowListener;
+ import java.util.ResourceBundle;
+ import javax.swing.JFrame;
+ import javax.swing.JPanel;
+ import javax.swing.JSplitPane;
+
+ import jexer.backend.SwingBackend;
+
+ /**
+ * This class is the main driver for a simple demonstration of Jexer's
+ * capabilities. It shows two Swing demo applications running in the same
+ * Swing UI.
+ */
+ public class Demo5 implements WindowListener {
+
+ /**
+ * Translated strings.
+ */
+ private static final ResourceBundle i18n = ResourceBundle.getBundle(Demo5.class.getName());
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The first demo application instance.
+ */
+ DemoApplication app1 = null;
+
+ /**
+ * The second demo application instance.
+ */
+ DemoApplication app2 = null;
+
+ // ------------------------------------------------------------------------
+ // WindowListener ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Pass window events into the event queue.
+ *
+ * @param event window event received
+ */
+ public void windowActivated(final WindowEvent event) {
+ // Ignore
+ }
+
+ /**
+ * Pass window events into the event queue.
+ *
+ * @param event window event received
+ */
+ public void windowClosed(final WindowEvent event) {
+ // Ignore
+ }
+
+ /**
+ * Pass window events into the event queue.
+ *
+ * @param event window event received
+ */
+ public void windowClosing(final WindowEvent event) {
+ if (app1 != null) {
+ app1.exit();
+ }
+ if (app2 != null) {
+ app2.exit();
+ }
+ }
+
+ /**
+ * Pass window events into the event queue.
+ *
+ * @param event window event received
+ */
+ public void windowDeactivated(final WindowEvent event) {
+ // Ignore
+ }
+
+ /**
+ * Pass window events into the event queue.
+ *
+ * @param event window event received
+ */
+ public void windowDeiconified(final WindowEvent event) {
+ // Ignore
+ }
+
+ /**
+ * Pass window events into the event queue.
+ *
+ * @param event window event received
+ */
+ public void windowIconified(final WindowEvent event) {
+ // Ignore
+ }
+
+ /**
+ * Pass window events into the event queue.
+ *
+ * @param event window event received
+ */
+ public void windowOpened(final WindowEvent event) {
+ // Ignore
+ }
+
+ // ------------------------------------------------------------------------
+ // Demo5 ------------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Run two demo applications in separate panes.
+ */
+ private void addApplications() {
+
+ /*
+ * In this demo we will create two swing panels with two
+ * independently running applications, each with a different font
+ * size.
+ */
+
+ /*
+ * First we create a panel to put it on. We need this to pass to
+ * SwingBackend's constructor, so that it knows not to create a new
+ * frame.
+ */
+ JPanel app1Panel = new JPanel();
+
+ /*
+ * Next, we create the Swing backend. The "listener" (second
+ * argument, set to null) is what the backend wakes up on every event
+ * received. Typically this is the TApplication. TApplication sets
+ * it in its constructor, so we can pass null here and be fine.
+ */
+ SwingBackend app1Backend = new SwingBackend(app1Panel, null,
+ 80, 25, 16);
+ // Now that we have the backend, construct the TApplication.
+ app1 = new DemoApplication(app1Backend);
+
+ /*
+ * The second panel is the same sequence, except that we also change
+ * the font from the default Terminus to JVM monospaced.
+ */
+ JPanel app2Panel = new JPanel();
+ SwingBackend app2Backend = new SwingBackend(app2Panel, null,
+ 80, 25, 18);
+ app2 = new DemoApplication(app2Backend);
+ Font font = new Font(Font.MONOSPACED, Font.PLAIN, 18);
+ app2Backend.setFont(font);
+
+ /*
+ * Now that the applications are ready, spin them off on their
+ * threads.
+ */
+ (new Thread(app1)).start();
+ (new Thread(app2)).start();
+
+ /*
+ * The rest of this is standard Swing. Set up a frame, a split pane,
+ * put each of the panels on it, and make it visible.
+ */
+ JFrame frame = new JFrame();
+ frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
+ frame.addWindowListener(this);
+ JSplitPane mainPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,
+ app1Panel, app2Panel);
+ mainPane.setOneTouchExpandable(true);
+ mainPane.setDividerLocation(500);
+ mainPane.setDividerSize(6);
+ mainPane.setBorder(null);
+ frame.setContentPane(mainPane);
+
+ frame.setTitle(i18n.getString("frameTitle"));
+ frame.setSize(1000, 640);
+ frame.setVisible(true);
+ }
+
+ /**
+ * Main entry point.
+ *
+ * @param args Command line arguments
+ */
+ public static void main(final String [] args) {
+ try {
+ Demo5 demo = new Demo5();
+ demo.addApplications();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ }
--- /dev/null
+ frameTitle=Two Jexer Apps In One Swing UI
--- /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.util.ResourceBundle;
+
+ import jexer.TApplication;
+ import jexer.backend.*;
+ import jexer.demos.DemoApplication;
+
+ /**
+ * This class shows off the use of MultiBackend and MultiScreen.
+ */
+ public class Demo6 {
+
+ /**
+ * Translated strings.
+ */
+ private static final ResourceBundle i18n = ResourceBundle.getBundle(Demo6.class.getName());
+
+ // ------------------------------------------------------------------------
+ // Demo6 ------------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Main entry point.
+ *
+ * @param args Command line arguments
+ */
+ public static void main(final String [] args) {
+ try {
+
+ /*
+ * In this demo we will create two applications spanning three
+ * screens. One of the applications will have both an ECMA48
+ * screen and a Swing screen, with all I/O mirrored between them.
+ * The second application will have a Swing screen containing a
+ * window showing the first application, also mirroring I/O
+ * between the window and the other two screens.
+ */
+
+ /*
+ * We create the first screen and use it to establish a
+ * MultiBackend.
+ */
+ ECMA48Backend ecmaBackend = new ECMA48Backend();
+ MultiBackend multiBackend = new MultiBackend(ecmaBackend);
+
+ /*
+ * Now we create the first application (a standard demo).
+ */
+ DemoApplication demoApp = new DemoApplication(multiBackend);
+
+ /*
+ * We will need the width and height of the ECMA48 screen, so get
+ * the Screen reference now.
+ */
+ Screen multiScreen = multiBackend.getScreen();
+
+ /*
+ * Now we create the second screen (backend) for the first
+ * application. It will be the same size as the ECMA48 screen,
+ * with a font size of 16 points.
+ */
+ SwingBackend swingBackend = new SwingBackend(multiScreen.getWidth(),
+ multiScreen.getHeight(), 16);
+
+ /*
+ * Add this screen to the MultiBackend, and at this point we have
+ * one demo application spanning two physical screens.
+ */
+ multiBackend.addBackend(swingBackend);
+ multiBackend.setListener(demoApp);
+
+ /*
+ * Time for the second application. This one will have a single
+ * window mirroring the contents of the first application. Let's
+ * make it a little larger than the first application's
+ * width/height.
+ */
+ int width = multiScreen.getWidth();
+ int height = multiScreen.getHeight();
+
+ /*
+ * Make a new Swing window for the second application.
+ */
+ SwingBackend monitorBackend = new SwingBackend(width + 5,
+ height + 5, 16);
+
+ /*
+ * Setup the second application, give it the basic file and
+ * window menus.
+ */
+ TApplication monitor = new TApplication(monitorBackend);
+ monitor.addToolMenu();
+ monitor.addFileMenu();
+ monitor.addWindowMenu();
+
+ /*
+ * Now add the third screen to the first application. We want to
+ * change the object it locks on in its draw() method to the
+ * MultiScreen, that will dramatically reduce (not totally
+ * eliminate) screen tearing/artifacts.
+ */
+ TWindowBackend windowBackend = new TWindowBackend(demoApp,
+ monitor, i18n.getString("monitorWindow"),
+ width + 2, height + 2);
+ windowBackend.setDrawLock(multiScreen);
+ windowBackend.setOtherApplication(demoApp);
+ multiBackend.addBackend(windowBackend);
+
+ /*
+ * Three screens, two applications: spin them up!
+ */
+ (new Thread(demoApp)).start();
+ (new Thread(monitor)).start();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ }
--- /dev/null
+ monitorWindow=Monitor Window
--- /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.util.ResourceBundle;
+
+ import jexer.TApplication;
+ import jexer.TPanel;
+ import jexer.TText;
+ import jexer.TWindow;
+ import jexer.layout.BoxLayoutManager;
+
+ /**
+ * This class shows off BoxLayout and TPanel.
+ */
+ public class Demo7 {
+
+ /**
+ * Translated strings.
+ */
+ private static final ResourceBundle i18n = ResourceBundle.getBundle(Demo7.class.getName());
+
+ // ------------------------------------------------------------------------
+ // Demo7 ------------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Main entry point.
+ *
+ * @param args Command line arguments
+ */
+ public static void main(final String [] args) throws Exception {
+ // This demo will build everything "from the outside".
+
+ // Swing is the default backend on Windows unless explicitly
+ // overridden by jexer.Swing.
+ TApplication.BackendType backendType = TApplication.BackendType.XTERM;
+ if (System.getProperty("os.name").startsWith("Windows")) {
+ backendType = TApplication.BackendType.SWING;
+ }
+ if (System.getProperty("os.name").startsWith("Mac")) {
+ backendType = TApplication.BackendType.SWING;
+ }
+ if (System.getProperty("jexer.Swing") != null) {
+ if (System.getProperty("jexer.Swing", "false").equals("true")) {
+ backendType = TApplication.BackendType.SWING;
+ } else {
+ backendType = TApplication.BackendType.XTERM;
+ }
+ }
+ TApplication app = new TApplication(backendType);
+ app.addToolMenu();
+ app.addFileMenu();
+ TWindow window = new TWindow(app, i18n.getString("windowTitle"),
+ 60, 22);
+ window.setLayoutManager(new BoxLayoutManager(window.getWidth() - 2,
+ window.getHeight() - 2, false));
+
+ TPanel right = window.addPanel(0, 0, 10, 10);
+ TPanel left = window.addPanel(0, 0, 10, 10);
+ right.setLayoutManager(new BoxLayoutManager(right.getWidth(),
+ right.getHeight(), true));
+ left.setLayoutManager(new BoxLayoutManager(left.getWidth(),
+ left.getHeight(), true));
+
+ left.addText("C1", 0, 0, left.getWidth(), left.getHeight());
+ left.addText("C2", 0, 0, left.getWidth(), left.getHeight());
+ left.addText("C3", 0, 0, left.getWidth(), left.getHeight());
+ right.addText("C4", 0, 0, right.getWidth(), right.getHeight());
+ right.addText("C5", 0, 0, right.getWidth(), right.getHeight());
+ right.addText("C6", 0, 0, right.getWidth(), right.getHeight());
+
+ app.run();
+ }
+
+ }
--- /dev/null
+ windowTitle=BoxLayoutManager Demo
--- /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.io.File;
+ import java.io.InputStream;
+ import java.io.IOException;
+ import java.io.OutputStream;
+ import java.io.PrintWriter;
+ import java.io.Reader;
+ import java.io.UnsupportedEncodingException;
+ import java.util.ResourceBundle;
+
+ import jexer.TApplication;
+ import jexer.TEditColorThemeWindow;
+ import jexer.TEditorWindow;
+ import jexer.event.TMenuEvent;
+ import jexer.menu.TMenu;
+ import jexer.menu.TMenuItem;
+ import jexer.menu.TSubMenu;
+ import jexer.backend.Backend;
+ import jexer.backend.SwingTerminal;
+
+ /**
+ * The demo application itself.
+ */
+ public class DemoApplication extends TApplication {
+
+ /**
+ * Translated strings.
+ */
+ private static final ResourceBundle i18n = ResourceBundle.getBundle(DemoApplication.class.getName());
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param input an InputStream connected to the remote user, or null for
+ * System.in. If System.in is used, then on non-Windows systems it will
+ * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
+ * mode. input is always converted to a Reader with UTF-8 encoding.
+ * @param output an OutputStream connected to the remote user, or null
+ * for System.out. output is always converted to a Writer with UTF-8
+ * encoding.
+ * @throws UnsupportedEncodingException if an exception is thrown when
+ * creating the InputStreamReader
+ */
+ public DemoApplication(final InputStream input,
+ final OutputStream output) throws UnsupportedEncodingException {
+ super(input, output);
+ addAllWidgets();
+
+ getBackend().setTitle(i18n.getString("applicationTitle"));
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param input the InputStream underlying 'reader'. Its available()
+ * method is used to determine if reader.read() will block or not.
+ * @param reader a Reader connected to the remote user.
+ * @param writer a PrintWriter connected to the remote user.
+ * @param setRawMode if true, set System.in into raw mode with stty.
+ * This should in general not be used. It is here solely for Demo3,
+ * which uses System.in.
+ * @throws IllegalArgumentException if input, reader, or writer are null.
+ */
+ public DemoApplication(final InputStream input, final Reader reader,
+ final PrintWriter writer, final boolean setRawMode) {
+ super(input, reader, writer, setRawMode);
+ addAllWidgets();
+
+ getBackend().setTitle(i18n.getString("applicationTitle"));
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param input the InputStream underlying 'reader'. Its available()
+ * method is used to determine if reader.read() will block or not.
+ * @param reader a Reader connected to the remote user.
+ * @param writer a PrintWriter connected to the remote user.
+ * @throws IllegalArgumentException if input, reader, or writer are null.
+ */
+ public DemoApplication(final InputStream input, final Reader reader,
+ final PrintWriter writer) {
+
+ this(input, reader, writer, false);
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param backend a Backend that is already ready to go.
+ */
+ public DemoApplication(final Backend backend) {
+ super(backend);
+
+ addAllWidgets();
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param backendType one of the TApplication.BackendType values
+ * @throws Exception if TApplication can't instantiate the Backend.
+ */
+ public DemoApplication(final BackendType backendType) throws Exception {
+ // For the Swing demo, use an initial size of 82x28 so that a
+ // terminal window precisely fits the window.
+ super(backendType, (backendType == BackendType.SWING ? 82 : -1),
+ (backendType == BackendType.SWING ? 28 : -1), 20);
+ addAllWidgets();
+ getBackend().setTitle(i18n.getString("applicationTitle"));
+ }
+
+ // ------------------------------------------------------------------------
+ // TApplication -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Handle menu events.
+ *
+ * @param menu menu event
+ * @return if true, the event was processed and should not be passed onto
+ * a window
+ */
+ @Override
+ public boolean onMenu(final TMenuEvent menu) {
+
+ if (menu.getId() == 3000) {
+ // Bigger +2
+ assert (getScreen() instanceof SwingTerminal);
+ SwingTerminal terminal = (SwingTerminal) getScreen();
+ terminal.setFontSize(terminal.getFontSize() + 2);
+ return true;
+ }
+ if (menu.getId() == 3001) {
+ // Smaller -2
+ assert (getScreen() instanceof SwingTerminal);
+ SwingTerminal terminal = (SwingTerminal) getScreen();
+ terminal.setFontSize(terminal.getFontSize() - 2);
+ return true;
+ }
+
+ if (menu.getId() == 2050) {
+ new TEditColorThemeWindow(this);
+ return true;
+ }
+
+ if (menu.getId() == TMenu.MID_OPEN_FILE) {
+ try {
+ String filename = fileOpenBox(".");
+ if (filename != null) {
+ try {
+ new TEditorWindow(this, new File(filename));
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return true;
+ }
+ return super.onMenu(menu);
+ }
+
+ // ------------------------------------------------------------------------
+ // DemoApplication --------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Add all the widgets of the demo.
+ */
+ private void addAllWidgets() {
+ new DemoMainWindow(this);
+
+ // Add the menus
+ addToolMenu();
+ addFileMenu();
+ addEditMenu();
+
+ TMenu demoMenu = addMenu(i18n.getString("demo"));
+ TMenuItem item = demoMenu.addItem(2000, i18n.getString("checkable"));
+ item.setCheckable(true);
+ item = demoMenu.addItem(2001, i18n.getString("disabled"));
+ item.setEnabled(false);
+ item = demoMenu.addItem(2002, i18n.getString("normal"));
+ TSubMenu subMenu = demoMenu.addSubMenu(i18n.getString("subMenu"));
+ item = demoMenu.addItem(2010, i18n.getString("normal"));
+ item = demoMenu.addItem(2050, i18n.getString("colors"));
+
+ item = subMenu.addItem(2000, i18n.getString("checkableSub"));
+ item.setCheckable(true);
+ item = subMenu.addItem(2001, i18n.getString("disabledSub"));
+ item.setEnabled(false);
+ item = subMenu.addItem(2002, i18n.getString("normalSub"));
+
+ subMenu = subMenu.addSubMenu(i18n.getString("subMenu"));
+ item = subMenu.addItem(2000, i18n.getString("checkableSub"));
+ item.setCheckable(true);
+ item = subMenu.addItem(2001, i18n.getString("disabledSub"));
+ item.setEnabled(false);
+ item = subMenu.addItem(2002, i18n.getString("normalSub"));
+
+ if (getScreen() instanceof SwingTerminal) {
+ TMenu swingMenu = addMenu(i18n.getString("swing"));
+ item = swingMenu.addItem(3000, i18n.getString("bigger"));
+ item = swingMenu.addItem(3001, i18n.getString("smaller"));
+ }
+
+ addTableMenu();
+ addWindowMenu();
+ addHelpMenu();
+ }
+
+ }
--- /dev/null
+ applicationTitle=Demo Application
+
+ demo=&Demo
+ checkable=&Checkable
+ disabled=Disabled
+ normal=&Normal
+ subMenu=Sub-&Menu
+ normal=N&ormal A&&D
+ colors=Co&lors...
+ checkableSub=&Checkable (sub)
+ disabledSub=Disabled (sub)
+ normalSub=&Normal (sub)
+ swing=Swin&g
+ bigger=&Bigger +2
+ smaller=&Smaller -2
--- /dev/null
+ /*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+ package jexer.demos;
+
+ import java.text.MessageFormat;
+ import java.util.ArrayList;
+ import java.util.List;
+ import java.util.ResourceBundle;
+
+ import jexer.TAction;
+ import jexer.TApplication;
+ import jexer.TComboBox;
+ import jexer.TMessageBox;
+ import jexer.TRadioGroup;
+ import jexer.TWindow;
+ import jexer.layout.StretchLayoutManager;
+ import static jexer.TCommand.*;
+ import static jexer.TKeypress.*;
+
+ /**
+ * This window demonstates the TRadioGroup, TRadioButton, and TCheckBox
+ * widgets.
+ */
+ public class DemoCheckBoxWindow extends TWindow {
+
+ /**
+ * Translated strings.
+ */
+ private static final ResourceBundle i18n = ResourceBundle.getBundle(DemoCheckBoxWindow.class.getName());
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Combo box. Has to be at class scope so that it can be accessed by the
+ * anonymous TAction class.
+ */
+ TComboBox comboBox = null;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Constructor.
+ *
+ * @param parent the main application
+ */
+ DemoCheckBoxWindow(final TApplication parent) {
+ this(parent, CENTERED | RESIZABLE);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param parent the main application
+ * @param flags bitmask of MODAL, CENTERED, or RESIZABLE
+ */
+ DemoCheckBoxWindow(final TApplication parent, final int flags) {
+ // Construct a demo window. X and Y don't matter because it will be
+ // centered on screen.
+ super(parent, i18n.getString("windowTitle"), 0, 0, 60, 17, flags);
+
+ setLayoutManager(new StretchLayoutManager(getWidth() - 2,
+ getHeight() - 2));
+
+ int row = 1;
+
+ // Add some widgets
+ addLabel(i18n.getString("checkBoxLabel1"), 1, row);
+ addCheckBox(35, row++, i18n.getString("checkBoxText1"), false);
+ addLabel(i18n.getString("checkBoxLabel2"), 1, row);
+ addCheckBox(35, row++, i18n.getString("checkBoxText2"), true);
+ row += 2;
+
+ TRadioGroup group = addRadioGroup(1, row,
+ i18n.getString("radioGroupTitle"));
+ group.addRadioButton(i18n.getString("radioOption1"));
+ group.addRadioButton(i18n.getString("radioOption2"));
+ group.addRadioButton(i18n.getString("radioOption3"));
+
+ List<String> comboValues = new ArrayList<String>();
+ comboValues.add(i18n.getString("comboBoxString0"));
+ comboValues.add(i18n.getString("comboBoxString1"));
+ comboValues.add(i18n.getString("comboBoxString2"));
+ comboValues.add(i18n.getString("comboBoxString3"));
+ comboValues.add(i18n.getString("comboBoxString4"));
+ comboValues.add(i18n.getString("comboBoxString5"));
+ comboValues.add(i18n.getString("comboBoxString6"));
+ comboValues.add(i18n.getString("comboBoxString7"));
+ comboValues.add(i18n.getString("comboBoxString8"));
+ comboValues.add(i18n.getString("comboBoxString9"));
+ comboValues.add(i18n.getString("comboBoxString10"));
+
+ comboBox = addComboBox(35, row, 12, comboValues, 2, 6,
+ new TAction() {
+ public void DO() {
+ getApplication().messageBox(i18n.getString("messageBoxTitle"),
+ MessageFormat.format(i18n.getString("messageBoxPrompt"),
+ comboBox.getText()),
+ TMessageBox.Type.OK);
+ }
+ }
+ );
+
+ addButton(i18n.getString("closeWindow"),
+ (getWidth() - 14) / 2, getHeight() - 4,
+ new TAction() {
+ public void DO() {
+ DemoCheckBoxWindow.this.getApplication()
+ .closeWindow(DemoCheckBoxWindow.this);
+ }
+ }
+ );
+
+ statusBar = newStatusBar(i18n.getString("statusBar"));
+ statusBar.addShortcutKeypress(kbF1, cmHelp,
+ i18n.getString("statusBarHelp"));
+ statusBar.addShortcutKeypress(kbF2, cmShell,
+ i18n.getString("statusBarShell"));
+ statusBar.addShortcutKeypress(kbF3, cmOpen,
+ i18n.getString("statusBarOpen"));
+ statusBar.addShortcutKeypress(kbF10, cmExit,
+ i18n.getString("statusBarExit"));
+ }
+
+ }
--- /dev/null
+ windowTitle=Radiobuttons, CheckBoxes, and ComboBox
+
+ statusBar=Radiobuttons and checkboxes
+ statusBarHelp=Help
+ statusBarShell=Shell
+ statusBarOpen=Open
+ statusBarExit=Exit
+
+ checkBoxLabel1=Check box example 1
+ checkBoxText1=CheckBox 1
+ checkBoxLabel2=Check box example 2
+ checkBoxText2=CheckBox 2
+ radioGroupTitle=Group 1
+ radioOption1=Radio option 1
+ radioOption2=Radio option 2
+ radioOption3=Radio option 3
+ comboBoxString0=String 0
+ comboBoxString1=String 1
+ comboBoxString2=String 2
+ comboBoxString3=String 3
+ comboBoxString4=String 4
+ comboBoxString5=String 5
+ comboBoxString6=String 6
+ comboBoxString7=String 7
+ comboBoxString8=String 8
+ comboBoxString9=String 9
+ comboBoxString10=String 10
+ messageBoxTitle=ComboBox
+ messageBoxPrompt=You selected the following value:\n\n{0}\n
+ closeWindow=&Close Window
--- /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.util.ResourceBundle;
+
+ import jexer.TApplication;
+ import jexer.TEditorWidget;
+ import jexer.TWidget;
+ import jexer.TWindow;
+ import jexer.event.TResizeEvent;
+ import static jexer.TCommand.*;
+ import static jexer.TKeypress.*;
+
+ /**
+ * This window demonstates the TEditor widget.
+ */
+ public class DemoEditorWindow extends TWindow {
+
+ /**
+ * Translated strings.
+ */
+ private static final ResourceBundle i18n = ResourceBundle.getBundle(DemoEditorWindow.class.getName());
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Hang onto my TEditor so I can resize it with the window.
+ */
+ private TEditorWidget editField;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor makes a text window out of any string.
+ *
+ * @param parent the main application
+ * @param title the text string
+ * @param text the text string
+ */
+ public DemoEditorWindow(final TApplication parent, final String title,
+ final String text) {
+
+ super(parent, title, 0, 0, 44, 22, RESIZABLE);
+ editField = addEditor(text, 0, 0, 42, 20);
+
+ statusBar = newStatusBar(i18n.getString("statusBar"));
+ statusBar.addShortcutKeypress(kbF1, cmHelp,
+ i18n.getString("statusBarHelp"));
+ statusBar.addShortcutKeypress(kbF2, cmShell,
+ i18n.getString("statusBarShell"));
+ statusBar.addShortcutKeypress(kbF10, cmExit,
+ i18n.getString("statusBarExit"));
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param parent the main application
+ */
+ public DemoEditorWindow(final TApplication parent) {
+ this(parent, i18n.getString("windowTitle"),
+ "This is an example of an editable text field. Some example text follows.\n" +
+ "\n" +
+ "This library implements a text-based windowing system loosely\n" +
+ "reminiscent of Borland's [Turbo\n" +
+ "Vision](http://en.wikipedia.org/wiki/Turbo_Vision) library. For those\n" +
+ "wishing to use the actual C++ Turbo Vision library, see [Sergio\n" +
+ "Sigala's updated version](http://tvision.sourceforge.net/) that runs\n" +
+ "on many more platforms.\n" +
+ "\n" +
+ "This library is licensed MIT. See the file LICENSE for the full license\n" +
+ "for the details.\n" +
+ "\n" +
+ "package jexer.demos;\n" +
+ "\n" +
+ "import jexer.*;\n" +
+ "import jexer.event.*;\n" +
+ "import static jexer.TCommand.*;\n" +
+ "import static jexer.TKeypress.*;\n" +
+ "\n" +
+ "/**\n" +
+ " * This window demonstates the TText, THScroller, and TVScroller widgets.\n" +
+ " */\n" +
+ "public class DemoEditorWindow extends TWindow {\n" +
+ "\n" +
+ "1 2 3 123\n" +
+ "\n"
+ );
+
+ }
+
+ // ------------------------------------------------------------------------
+ // TWindow ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Handle window/screen resize events.
+ *
+ * @param event resize event
+ */
+ @Override
+ public void onResize(final TResizeEvent event) {
+ if (event.getType() == TResizeEvent.Type.WIDGET) {
+ // Resize the text field
+ TResizeEvent editSize = new TResizeEvent(TResizeEvent.Type.WIDGET,
+ event.getWidth() - 2, event.getHeight() - 2);
+ editField.onResize(editSize);
+ return;
+ }
+
+ // Pass to children instead
+ for (TWidget widget: getChildren()) {
+ widget.onResize(event);
+ }
+ }
+
+ }
--- /dev/null
+ windowTitle=Editor
+
+ statusBar=Editable text demo window
+ statusBarHelp=Help
+ statusBarShell=Shell
+ statusBarExit=Exit
--- /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.io.File;
+ import java.io.IOException;
+ import java.text.MessageFormat;
+ import java.util.ResourceBundle;
+
+ import jexer.TAction;
+ import jexer.TApplication;
+ import jexer.TEditColorThemeWindow;
+ import jexer.TEditorWindow;
+ import jexer.TLabel;
+ import jexer.TProgressBar;
+ import jexer.TTableWindow;
+ import jexer.TTimer;
+ import jexer.TWidget;
+ import jexer.TWindow;
+ import jexer.event.TCommandEvent;
+ import jexer.layout.StretchLayoutManager;
+ import static jexer.TCommand.*;
+ import static jexer.TKeypress.*;
+
+ /**
+ * This is the main "demo" application window. It makes use of the TTimer,
+ * TProgressBox, TLabel, TButton, and TField widgets.
+ */
+ public class DemoMainWindow extends TWindow {
+
+ /**
+ * Translated strings.
+ */
+ private static final ResourceBundle i18n = ResourceBundle.getBundle(DemoMainWindow.class.getName());
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Timer that increments a number.
+ */
+ private TTimer timer1;
+
+ /**
+ * Timer that increments a number.
+ */
+ private TTimer timer2;
+
+ /**
+ * Timer label is updated with timer ticks.
+ */
+ TLabel timerLabel;
+
+ /**
+ * Timer increment used by the timer loop. Has to be at class scope so
+ * that it can be accessed by the anonymous TAction class.
+ */
+ int timer1I = 0;
+
+ /**
+ * Timer increment used by the timer loop. Has to be at class scope so
+ * that it can be accessed by the anonymous TAction class.
+ */
+ int timer2I = 0;
+
+ /**
+ * Progress bar used by the timer loop. Has to be at class scope so that
+ * it can be accessed by the anonymous TAction class.
+ */
+ TProgressBar progressBar1;
+
+ /**
+ * Progress bar used by the timer loop. Has to be at class scope so that
+ * it can be accessed by the anonymous TAction class.
+ */
+ TProgressBar progressBar2;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Construct demo window. It will be centered on screen.
+ *
+ * @param parent the main application
+ */
+ public DemoMainWindow(final TApplication parent) {
+ this(parent, CENTERED | RESIZABLE);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param parent the main application
+ * @param flags bitmask of MODAL, CENTERED, or RESIZABLE
+ */
+ private DemoMainWindow(final TApplication parent, final int flags) {
+ // Construct a demo window. X and Y don't matter because it will be
+ // centered on screen.
+ super(parent, i18n.getString("windowTitle"), 0, 0, 64, 23, flags);
+
+ setLayoutManager(new StretchLayoutManager(getWidth() - 2,
+ getHeight() - 2));
+
+ int row = 1;
+
+ // Add some widgets
+ addLabel(i18n.getString("messageBoxLabel"), 1, row);
+ TWidget first = addButton(i18n.getString("messageBoxButton"), 35, row,
+ new TAction() {
+ public void DO() {
+ new DemoMsgBoxWindow(getApplication());
+ }
+ }
+ );
+ row += 2;
+
+ addLabel(i18n.getString("openModalLabel"), 1, row);
+ addButton(i18n.getString("openModalButton"), 35, row,
+ new TAction() {
+ public void DO() {
+ new DemoMainWindow(getApplication(), MODAL);
+ }
+ }
+ );
+ row += 2;
+
+ addLabel(i18n.getString("textFieldLabel"), 1, row);
+ addButton(i18n.getString("textFieldButton"), 35, row,
+ new TAction() {
+ public void DO() {
+ new DemoTextFieldWindow(getApplication());
+ }
+ }
+ );
+ row += 2;
+
+ addLabel(i18n.getString("radioButtonLabel"), 1, row);
+ addButton(i18n.getString("radioButtonButton"), 35, row,
+ new TAction() {
+ public void DO() {
+ new DemoCheckBoxWindow(getApplication());
+ }
+ }
+ );
+ row += 2;
+
+ addLabel(i18n.getString("editorLabel"), 1, row);
+ addButton(i18n.getString("editorButton1"), 35, row,
+ new TAction() {
+ public void DO() {
+ new DemoEditorWindow(getApplication());
+ }
+ }
+ );
+ addButton(i18n.getString("editorButton2"), 48, row,
+ new TAction() {
+ public void DO() {
+ new TEditorWindow(getApplication());
+ }
+ }
+ );
+ row += 2;
+
+ addLabel(i18n.getString("textAreaLabel"), 1, row);
+ addButton(i18n.getString("textAreaButton"), 35, row,
+ new TAction() {
+ public void DO() {
+ new DemoTextWindow(getApplication());
+ }
+ }
+ );
+ row += 2;
+
+ addLabel(i18n.getString("ttableLabel"), 1, row);
+ addButton(i18n.getString("ttableButton1"), 35, row,
+ new TAction() {
+ public void DO() {
+ new DemoTableWindow(getApplication(),
+ i18n.getString("tableWidgetDemo"));
+ }
+ }
+ );
+ addButton(i18n.getString("ttableButton2"), 48, row,
+ new TAction() {
+ public void DO() {
+ new TTableWindow(getApplication(),
+ i18n.getString("tableDemo"));
+ }
+ }
+ );
+ row += 2;
+
+ addLabel(i18n.getString("treeViewLabel"), 1, row);
+ addButton(i18n.getString("treeViewButton"), 35, row,
+ new TAction() {
+ public void DO() {
+ try {
+ new DemoTreeViewWindow(getApplication());
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ );
+ row += 2;
+
+ addLabel(i18n.getString("terminalLabel"), 1, row);
+ addButton(i18n.getString("terminalButton"), 35, row,
+ new TAction() {
+ public void DO() {
+ getApplication().openTerminal(0, 0);
+ }
+ }
+ );
+ row += 2;
+
+ addLabel(i18n.getString("colorEditorLabel"), 1, row);
+ addButton(i18n.getString("colorEditorButton"), 35, row,
+ new TAction() {
+ public void DO() {
+ new TEditColorThemeWindow(getApplication());
+ }
+ }
+ );
+
+ row = 15;
+ progressBar1 = addProgressBar(48, row, 12, 0);
+ row++;
+ timerLabel = addLabel(i18n.getString("timerLabel"), 48, row);
+ timer1 = getApplication().addTimer(250, true,
+ new TAction() {
+
+ public void DO() {
+ timerLabel.setLabel(String.format(i18n.
+ getString("timerText"), timer1I));
+ timerLabel.setWidth(timerLabel.getLabel().length());
+ if (timer1I < 100) {
+ timer1I++;
+ } else {
+ timer1.setRecurring(false);
+ }
+ progressBar1.setValue(timer1I);
+ }
+ }
+ );
+
+ row += 2;
+ progressBar2 = addProgressBar(48, row, 12, 0);
+ progressBar2.setLeftBorderChar('\u255e');
+ progressBar2.setRightBorderChar('\u2561');
+ progressBar2.setCompletedChar('\u2592');
+ progressBar2.setRemainingChar('\u2550');
+ row++;
+ timer2 = getApplication().addTimer(125, true,
+ new TAction() {
+
+ public void DO() {
+ if (timer2I < 100) {
+ timer2I++;
+ } else {
+ timer2.setRecurring(false);
+ }
+ progressBar2.setValue(timer2I);
+ }
+ }
+ );
+
+ /*
+ addButton("Exception", 35, row + 3,
+ new TAction() {
+ public void DO() {
+ try {
+ throw new RuntimeException("FUBAR'd!");
+ } catch (Exception e) {
+ new jexer.TExceptionDialog(getApplication(), e);
+ }
+ }
+ }
+ );
+ */
+
+ activate(first);
+
+ statusBar = newStatusBar(i18n.getString("statusBar"));
+ statusBar.addShortcutKeypress(kbF1, cmHelp,
+ i18n.getString("statusBarHelp"));
+ statusBar.addShortcutKeypress(kbF2, cmShell,
+ i18n.getString("statusBarShell"));
+ statusBar.addShortcutKeypress(kbF3, cmOpen,
+ i18n.getString("statusBarOpen"));
+ statusBar.addShortcutKeypress(kbF10, cmExit,
+ i18n.getString("statusBarExit"));
+ }
+
+ // ------------------------------------------------------------------------
+ // TWindow ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * We need to override onClose so that the timer will no longer be called
+ * after we close the window. TTimers currently are completely unaware
+ * of the rest of the UI classes.
+ */
+ @Override
+ public void onClose() {
+ getApplication().removeTimer(timer1);
+ getApplication().removeTimer(timer2);
+ }
+
+ /**
+ * Method that subclasses can override to handle posted command events.
+ *
+ * @param command command event
+ */
+ @Override
+ public void onCommand(final TCommandEvent command) {
+ if (command.equals(cmOpen)) {
+ try {
+ String filename = fileOpenBox(".");
+ if (filename != null) {
+ try {
+ new TEditorWindow(getApplication(),
+ new File(filename));
+ } catch (IOException e) {
+ messageBox(i18n.getString("errorTitle"),
+ MessageFormat.format(i18n.
+ getString("errorReadingFile"), e.getMessage()));
+ }
+ }
+ } catch (IOException e) {
+ messageBox(i18n.getString("errorTitle"),
+ MessageFormat.format(i18n.
+ getString("errorOpeningFile"), e.getMessage()));
+ }
+ return;
+ }
+
+ // Didn't handle it, let children get it instead
+ super.onCommand(command);
+ }
+
+ }
--- /dev/null
+ windowTitle=Demo Window
+
+ statusBar=Demo Main Window
+ statusBarHelp=Help
+ statusBarShell=Shell
+ statusBarOpen=Open
+ statusBarExit=Exit
+
+ messageBoxLabel=Message Boxes
+ messageBoxButton=&MessageBoxes
+ openModalLabel=Open me as modal
+ openModalButton=M&odal
+ textFieldLabel=Text fields, calendar, spinner
+ textFieldButton=Field&s
+ radioButtonLabel=Radio buttons, checkbox, combobox
+ radioButtonButton=&CheckBoxes
+ editorLabel=Editor window
+ editorButton1=&1 Widget
+ editorButton2=&2 Window
+ ttableLabel=Editable Table
+ ttableButton1=&4 Widget
+ ttableButton2=&5 Window
+ textAreaLabel=Text areas
+ textAreaButton=&3 Text
+ treeViewLabel=Tree views
+ treeViewButton=Tree&View
+ terminalLabel=Terminal
+ terminalButton=Termi&nal
+ colorEditorLabel=Color editor
+ colorEditorButton=Co&lors
+ timerLabel=Timer
+ timerText=Timer: %d
+
+ errorTitle=Error
+ errorReadingFile=Error reading file: {0}
+ errorOpeningFile=Error opening file dialog: {0}
+
+ tableWidgetDemo=TTableWidget Demo
+ tableDemo=TTableWindow Demo
--- /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.text.MessageFormat;
+ import java.util.ResourceBundle;
+
+ import jexer.TAction;
+ import jexer.TApplication;
+ import jexer.TInputBox;
+ import jexer.TMessageBox;
+ import jexer.TWindow;
+ import jexer.layout.StretchLayoutManager;
+ import static jexer.TCommand.*;
+ import static jexer.TKeypress.*;
+
+ /**
+ * This window demonstates the TMessageBox and TInputBox widgets.
+ */
+ public class DemoMsgBoxWindow extends TWindow {
+
+ /**
+ * Translated strings.
+ */
+ private static final ResourceBundle i18n = ResourceBundle.getBundle(DemoMsgBoxWindow.class.getName());
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Constructor.
+ *
+ * @param parent the main application
+ */
+ DemoMsgBoxWindow(final TApplication parent) {
+ this(parent, TWindow.CENTERED | TWindow.RESIZABLE);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param parent the main application
+ * @param flags bitmask of MODAL, CENTERED, or RESIZABLE
+ */
+ DemoMsgBoxWindow(final TApplication parent, final int flags) {
+ // Construct a demo window. X and Y don't matter because it
+ // will be centered on screen.
+ super(parent, i18n.getString("windowTitle"), 0, 0, 64, 18, flags);
+
+ setLayoutManager(new StretchLayoutManager(getWidth() - 2,
+ getHeight() - 2));
+
+ int row = 1;
+
+ // Add some widgets
+ addLabel(i18n.getString("messageBoxLabel1"), 1, row);
+ addButton(i18n.getString("messageBoxButton1"), 35, row,
+ new TAction() {
+ public void DO() {
+ getApplication().messageBox(i18n.
+ getString("messageBoxTitle1"),
+ i18n.getString("messageBoxPrompt1"),
+ TMessageBox.Type.OK);
+ }
+ }
+ );
+ row += 2;
+
+ addLabel(i18n.getString("messageBoxLabel2"), 1, row);
+ addButton(i18n.getString("messageBoxButton2"), 35, row,
+ new TAction() {
+ public void DO() {
+ getApplication().messageBox(i18n.
+ getString("messageBoxTitle2"),
+ i18n.getString("messageBoxPrompt2"),
+ TMessageBox.Type.OKCANCEL);
+ }
+ }
+ );
+ row += 2;
+
+ addLabel(i18n.getString("messageBoxLabel3"), 1, row);
+ addButton(i18n.getString("messageBoxButton3"), 35, row,
+ new TAction() {
+ public void DO() {
+ getApplication().messageBox(i18n.
+ getString("messageBoxTitle3"),
+ i18n.getString("messageBoxPrompt3"),
+ TMessageBox.Type.YESNO);
+ }
+ }
+ );
+ row += 2;
+
+ addLabel(i18n.getString("messageBoxLabel4"), 1, row);
+ addButton(i18n.getString("messageBoxButton4"), 35, row,
+ new TAction() {
+ public void DO() {
+ getApplication().messageBox(i18n.
+ getString("messageBoxTitle4"),
+ i18n.getString("messageBoxPrompt4"),
+ TMessageBox.Type.YESNOCANCEL);
+ }
+ }
+ );
+ row += 2;
+
+ addLabel(i18n.getString("inputBoxLabel1"), 1, row);
+ addButton(i18n.getString("inputBoxButton1"), 35, row,
+ new TAction() {
+ public void DO() {
+ TInputBox in = getApplication().inputBox(i18n.
+ getString("inputBoxTitle1"),
+ i18n.getString("inputBoxPrompt1"),
+ i18n.getString("inputBoxInput1"));
+ getApplication().messageBox(i18n.
+ getString("inputBoxAnswerTitle1"),
+ MessageFormat.format(i18n.
+ getString("inputBoxAnswerPrompt1"), in.getText()));
+ }
+ }
+ );
+ row += 2;
+
+ addLabel(i18n.getString("inputBoxLabel2"), 1, row);
+ addButton(i18n.getString("inputBoxButton2"), 35, row,
+ new TAction() {
+ public void DO() {
+ TInputBox in = getApplication().inputBox(i18n.
+ getString("inputBoxTitle2"),
+ i18n.getString("inputBoxPrompt2"),
+ i18n.getString("inputBoxInput2"),
+ TInputBox.Type.OKCANCEL);
+ getApplication().messageBox(i18n.
+ getString("inputBoxAnswerTitle2"),
+ MessageFormat.format(i18n.
+ getString("inputBoxAnswerPrompt2"), in.getText(),
+ in.getResult()));
+ }
+ }
+ );
+ row += 2;
+
+ addButton(i18n.getString("closeWindow"),
+ (getWidth() - 14) / 2, getHeight() - 4,
+ new TAction() {
+ public void DO() {
+ getApplication().closeWindow(DemoMsgBoxWindow.this);
+ }
+ }
+ );
+
+ statusBar = newStatusBar(i18n.getString("statusBar"));
+ statusBar.addShortcutKeypress(kbF1, cmHelp,
+ i18n.getString("statusBarHelp"));
+ statusBar.addShortcutKeypress(kbF2, cmShell,
+ i18n.getString("statusBarShell"));
+ statusBar.addShortcutKeypress(kbF3, cmOpen,
+ i18n.getString("statusBarOpen"));
+ statusBar.addShortcutKeypress(kbF10, cmExit,
+ i18n.getString("statusBarExit"));
+ }
+ }
--- /dev/null
+ windowTitle=Message Boxes
+
+ statusBar=Message boxes
+ statusBarHelp=Help
+ statusBarShell=Shell
+ statusBarOpen=Open
+ statusBarExit=Exit
+
+ messageBoxLabel1=Default OK message box
+ messageBoxButton1=Open O&K MB
+ messageBoxTitle1=OK MessageBox
+ messageBoxPrompt1=This is an example of a OK MessageBox. This is the\ndefault MessageBox.\n\nNote that the MessageBox text can span multiple\nlines.\n\nThe default result (if someone hits the top-left\nclose button) is OK.\n
+
+ messageBoxLabel2=OK/Cancel message box
+ messageBoxButton2=O&pen OKC MB
+ messageBoxTitle2=OK/Cancel MessageBox
+ messageBoxPrompt2=This is an example of a OK/Cancel MessageBox.\n\nNote that the MessageBox text can span multiple\nlines.\n\nThe default result (if someone hits the top-leftclose button) is CANCEL.\n
+
+ messageBoxLabel3=Yes/No message box
+ messageBoxButton3=Open &YN MB
+ messageBoxTitle3=Yes/No MessageBox
+ messageBoxPrompt3=This is an example of a Yes/No MessageBox.\n\nNote that the MessageBox text can span multiple\nlines.\n\nThe default result (if someone hits the top-left\nclose button) is NO.\n
+
+ messageBoxLabel4=Yes/No/Cancel message box
+ messageBoxButton4=Ope&n YNC MB
+ messageBoxTitle4=Yes/No/Cancel MessageBox
+ messageBoxPrompt4=This is an example of a Yes/No/Cancel MessageBox.\n\nNote that the MessageBox text can span multiple\nlines.\n\nThe default result (if someone hits the top-left\nclose button) is CANCEL.\n
+
+ inputBoxLabel1=Input box 1
+ inputBoxButton1=Open &input box
+ inputBoxTitle1=Input Box
+ inputBoxPrompt1=This is an example of an InputBox.\n\nNote that the InputBox text can span multiple\nlines.\n
+ inputBoxInput1=some input text
+ inputBoxAnswerTitle1=Your InputBox Answer
+ inputBoxAnswerPrompt1=You entered: {0}
+
+ inputBoxLabel2=Input box 2
+ inputBoxButton2=Cance&llable input box
+ inputBoxTitle2=Input Box
+ inputBoxPrompt2=This is an example of an InputBox.\n\nNote that the InputBox text can span multiple\nlines.\nThis one has both OK and Cancel buttons.\n
+ inputBoxInput2=some input text
+ inputBoxAnswerTitle2=Your InputBox Answer
+ inputBoxAnswerPrompt2=You entered: {0} and pressed {1}
+
+ closeWindow=&Close Window
--- /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.util.ResourceBundle;
+
+ import jexer.TApplication;
+ import jexer.TTableWidget;
+ import jexer.TWidget;
+ import jexer.TWindow;
+ import jexer.event.TResizeEvent;
+ import static jexer.TCommand.*;
+ import static jexer.TKeypress.*;
+
+ /**
+ * This window demonstates the TTable widget.
+ */
+ public class DemoTableWindow extends TWindow {
+
+ /**
+ * Translated strings.
+ */
+ private static final ResourceBundle i18n = ResourceBundle.getBundle(DemoTableWindow.class.getName());
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Hang onto my TTable so I can resize it with the window.
+ */
+ private TTableWidget tableField;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor makes a text window out of any string.
+ *
+ * @param parent the main application
+ * @param title the text string
+ */
+ public DemoTableWindow(final TApplication parent, final String title) {
+
+ super(parent, title, 0, 0, 44, 22, RESIZABLE);
+ tableField = new TTableWidget(this, 0, 0, 42, 20);
+
+ statusBar = newStatusBar(i18n.getString("statusBar"));
+ statusBar.addShortcutKeypress(kbF1, cmHelp,
+ i18n.getString("statusBarHelp"));
+ statusBar.addShortcutKeypress(kbF2, cmShell,
+ i18n.getString("statusBarShell"));
+ statusBar.addShortcutKeypress(kbF10, cmExit,
+ i18n.getString("statusBarExit"));
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param parent the main application
+ */
+ public DemoTableWindow(final TApplication parent) {
+ this(parent, i18n.getString("windowTitle"));
+ }
+
+ // ------------------------------------------------------------------------
+ // TWindow ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Handle window/screen resize events.
+ *
+ * @param event resize event
+ */
+ @Override
+ public void onResize(final TResizeEvent event) {
+ if (event.getType() == TResizeEvent.Type.WIDGET) {
+ // Resize the text field
+ TResizeEvent tableSize = new TResizeEvent(TResizeEvent.Type.WIDGET,
+ event.getWidth() - 2, event.getHeight() - 2);
+ tableField.onResize(tableSize);
+ return;
+ }
+
+ // Pass to children instead
+ for (TWidget widget: getChildren()) {
+ widget.onResize(event);
+ }
+ }
+
+ }
--- /dev/null
+ windowTitle=Table
+
+ statusBar=Table datagrid demo window
+ statusBarHelp=Help
+ statusBarShell=Shell
+ statusBarExit=Exit
--- /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.text.MessageFormat;
+ import java.util.Calendar;
+ import java.util.Date;
+ import java.util.GregorianCalendar;
+ import java.util.Locale;
+ import java.util.ResourceBundle;
+
+ import jexer.TAction;
+ import jexer.TApplication;
+ import jexer.TCalendar;
+ import jexer.TField;
+ import jexer.TLabel;
+ import jexer.TMessageBox;
+ import jexer.TWindow;
+ import jexer.layout.StretchLayoutManager;
+ import static jexer.TCommand.*;
+ import static jexer.TKeypress.*;
+
+ /**
+ * This window demonstates the TField and TPasswordField widgets.
+ */
+ public class DemoTextFieldWindow extends TWindow {
+
+ /**
+ * Translated strings.
+ */
+ private static final ResourceBundle i18n = ResourceBundle.getBundle(DemoTextFieldWindow.class.getName());
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Calendar. Has to be at class scope so that it can be accessed by the
+ * anonymous TAction class.
+ */
+ TCalendar calendar = null;
+
+ /**
+ * Day of week label is updated with TSpinner clicks.
+ */
+ TLabel dayOfWeekLabel;
+
+ /**
+ * Day of week to demonstrate TSpinner. Has to be at class scope so that
+ * it can be accessed by the anonymous TAction class.
+ */
+ GregorianCalendar dayOfWeekCalendar = new GregorianCalendar();
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Constructor.
+ *
+ * @param parent the main application
+ */
+ DemoTextFieldWindow(final TApplication parent) {
+ this(parent, TWindow.CENTERED | TWindow.RESIZABLE);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param parent the main application
+ * @param flags bitmask of MODAL, CENTERED, or RESIZABLE
+ */
+ DemoTextFieldWindow(final TApplication parent, final int flags) {
+ // Construct a demo window. X and Y don't matter because it
+ // will be centered on screen.
+ super(parent, i18n.getString("windowTitle"), 0, 0, 60, 20, flags);
+
+ setLayoutManager(new StretchLayoutManager(getWidth() - 2,
+ getHeight() - 2));
+
+ int row = 1;
+
+ addLabel(i18n.getString("textField1"), 1, row);
+ addField(35, row++, 15, false, "Field text");
+ addLabel(i18n.getString("textField2"), 1, row);
+ addField(35, row++, 15, true);
+ addLabel(i18n.getString("textField3"), 1, row);
+ addPasswordField(35, row++, 15, false);
+ addLabel(i18n.getString("textField4"), 1, row);
+ addPasswordField(35, row++, 15, true, "hunter2");
+ addLabel(i18n.getString("textField5"), 1, row);
+ TField selected = addField(35, row++, 40, false,
+ i18n.getString("textField6"));
+ row += 1;
+
+ calendar = addCalendar(1, row++,
+ new TAction() {
+ public void DO() {
+ getApplication().messageBox(i18n.getString("calendarTitle"),
+ MessageFormat.format(i18n.getString("calendarMessage"),
+ new Date(calendar.getValue().getTimeInMillis())),
+ TMessageBox.Type.OK);
+ }
+ }
+ );
+
+ dayOfWeekLabel = addLabel("Wednesday-", 35, row - 1, "tmenu", false);
+ dayOfWeekLabel.setLabel(String.format("%-10s",
+ dayOfWeekCalendar.getDisplayName(Calendar.DAY_OF_WEEK,
+ Calendar.LONG, Locale.getDefault())));
+
+ addSpinner(35 + dayOfWeekLabel.getWidth(), row - 1,
+ new TAction() {
+ public void DO() {
+ dayOfWeekCalendar.add(Calendar.DAY_OF_WEEK, 1);
+ dayOfWeekLabel.setLabel(String.format("%-10s",
+ dayOfWeekCalendar.getDisplayName(
+ Calendar.DAY_OF_WEEK, Calendar.LONG,
+ Locale.getDefault())));
+ }
+ },
+ new TAction() {
+ public void DO() {
+ dayOfWeekCalendar.add(Calendar.DAY_OF_WEEK, -1);
+ dayOfWeekLabel.setLabel(String.format("%-10s",
+ dayOfWeekCalendar.getDisplayName(
+ Calendar.DAY_OF_WEEK, Calendar.LONG,
+ Locale.getDefault())));
+ }
+ }
+ );
+
+
+ addButton(i18n.getString("closeWindow"),
+ (getWidth() - 14) / 2, getHeight() - 4,
+ new TAction() {
+ public void DO() {
+ getApplication().closeWindow(DemoTextFieldWindow.this);
+ }
+ }
+ );
+
+ activate(selected);
+
+ statusBar = newStatusBar(i18n.getString("statusBar"));
+ statusBar.addShortcutKeypress(kbF1, cmHelp,
+ i18n.getString("statusBarHelp"));
+ statusBar.addShortcutKeypress(kbF2, cmShell,
+ i18n.getString("statusBarShell"));
+ statusBar.addShortcutKeypress(kbF3, cmOpen,
+ i18n.getString("statusBarOpen"));
+ statusBar.addShortcutKeypress(kbF10, cmExit,
+ i18n.getString("statusBarExit"));
+ }
+
+ }
--- /dev/null
+ windowTitle=Text Fields
+
+ statusBar=Text fields
+ statusBarHelp=Help
+ statusBarShell=Shell
+ statusBarOpen=Open
+ statusBarExit=Exit
+
+ textField1=Variable-width text field:
+ textField2=Fixed-width text field:
+ textField3=Variable-width password:
+ textField4=Fixed-width password:
+ textField5=Very long text field:
+ textField6=Very very long field text that should be outside the window
+ calendarTitle=Calendar
+ calendarMessage=You selected the following date:\n\n{0}\n
+ closeWindow=&Close Window
--- /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.util.ResourceBundle;
+
+ import jexer.TAction;
+ import jexer.TApplication;
+ import jexer.TText;
+ import jexer.TWidget;
+ import jexer.TWindow;
+ import jexer.event.TResizeEvent;
+ import jexer.menu.TMenu;
+ import static jexer.TCommand.*;
+ import static jexer.TKeypress.*;
+
+ /**
+ * This window demonstates the TText, THScroller, and TVScroller widgets.
+ */
+ public class DemoTextWindow extends TWindow {
+
+ /**
+ * Translated strings.
+ */
+ private static final ResourceBundle i18n = ResourceBundle.getBundle(DemoTextWindow.class.getName());
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Hang onto my TText so I can resize it with the window.
+ */
+ private TText textField;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor makes a text window out of any string.
+ *
+ * @param parent the main application
+ * @param title the text string
+ * @param text the text string
+ */
+ public DemoTextWindow(final TApplication parent, final String title,
+ final String text) {
+
+ super(parent, title, 0, 0, 44, 22, RESIZABLE);
+ textField = addText(text, 1, 3, 40, 16);
+
+ addButton(i18n.getString("left"), 1, 1, new TAction() {
+ public void DO() {
+ textField.leftJustify();
+ }
+ });
+
+ addButton(i18n.getString("center"), 10, 1, new TAction() {
+ public void DO() {
+ textField.centerJustify();
+ }
+ });
+
+ addButton(i18n.getString("right"), 21, 1, new TAction() {
+ public void DO() {
+ textField.rightJustify();
+ }
+ });
+
+ addButton(i18n.getString("full"), 31, 1, new TAction() {
+ public void DO() {
+ textField.fullJustify();
+ }
+ });
+
+ statusBar = newStatusBar(i18n.getString("statusBar"));
+ statusBar.addShortcutKeypress(kbF1, cmHelp,
+ i18n.getString("statusBarHelp"));
+ statusBar.addShortcutKeypress(kbF2, cmShell,
+ i18n.getString("statusBarShell"));
+ statusBar.addShortcutKeypress(kbF3, cmOpen,
+ i18n.getString("statusBarOpen"));
+ statusBar.addShortcutKeypress(kbF10, cmExit,
+ i18n.getString("statusBarExit"));
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param parent the main application
+ */
+ public DemoTextWindow(final TApplication parent) {
+ this(parent, i18n.getString("windowTitle"),
+ "This is an example of a reflowable text field. Some example text follows.\n" +
+ "\n" +
+ "Notice that some menu items should be disabled when this window has focus.\n" +
+ "\n" +
+ "This library implements a text-based windowing system loosely " +
+ "reminiscent of Borland's [Turbo " +
+ "Vision](http://en.wikipedia.org/wiki/Turbo_Vision) library. For those " +
+ "wishing to use the actual C++ Turbo Vision library, see [Sergio " +
+ "Sigala's updated version](http://tvision.sourceforge.net/) that runs " +
+ "on many more platforms.\n" +
+ "\n" +
+ "This library is licensed MIT. See the file LICENSE for the full license " +
+ "for the details.\n");
+
+ }
+
+ // ------------------------------------------------------------------------
+ // TWindow ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Handle window/screen resize events.
+ *
+ * @param event resize event
+ */
+ @Override
+ public void onResize(final TResizeEvent event) {
+ if (event.getType() == TResizeEvent.Type.WIDGET) {
+ // Resize the text field
+ TResizeEvent textSize = new TResizeEvent(TResizeEvent.Type.WIDGET,
+ event.getWidth() - 4, event.getHeight() - 6);
+ textField.onResize(textSize);
+ return;
+ }
+
+ // Pass to children instead
+ for (TWidget widget: getChildren()) {
+ widget.onResize(event);
+ }
+ }
+
+ /**
+ * Play with menu items.
+ */
+ public void onFocus() {
+ getApplication().enableMenuItem(2001);
+ getApplication().disableMenuItem(TMenu.MID_SHELL);
+ getApplication().disableMenuItem(TMenu.MID_EXIT);
+ }
+
+ /**
+ * Called by application.switchWindow() when another window gets the
+ * focus.
+ */
+ public void onUnfocus() {
+ getApplication().disableMenuItem(2001);
+ getApplication().enableMenuItem(TMenu.MID_SHELL);
+ getApplication().enableMenuItem(TMenu.MID_EXIT);
+ }
+
+ }
--- /dev/null
+ windowTitle=Text Area
+
+ statusBar=Reflowable text window
+ statusBarHelp=Help
+ statusBarShell=Shell
+ statusBarOpen=Open
+ statusBarExit=Exit
+
+ left=&Left
+ center=&Center
+ right=&Right
+ full=&Full
--- /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.io.IOException;
+ import java.util.ResourceBundle;
+
+ import jexer.TApplication;
+ import jexer.TWidget;
+ import jexer.TWindow;
+ import jexer.event.TResizeEvent;
+ import jexer.ttree.TDirectoryTreeItem;
+ import jexer.ttree.TTreeViewWidget;
+ import static jexer.TCommand.*;
+ import static jexer.TKeypress.*;
+
+ /**
+ * This window demonstates the TTreeView widget.
+ */
+ public class DemoTreeViewWindow extends TWindow {
+
+ /**
+ * Translated strings.
+ */
+ private static final ResourceBundle i18n = ResourceBundle.getBundle(DemoTreeViewWindow.class.getName());
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Hang onto my TTreeView so I can resize it with the window.
+ */
+ private TTreeViewWidget treeView;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param parent the main application
+ * @throws IOException if a java.io operation throws
+ */
+ public DemoTreeViewWindow(final TApplication parent) throws IOException {
+ super(parent, i18n.getString("windowTitle"), 0, 0, 44, 16,
+ TWindow.RESIZABLE);
+
+ // Load the treeview with "stuff"
+ treeView = addTreeViewWidget(1, 1, 40, 12);
+ new TDirectoryTreeItem(treeView, ".", true);
+
+ statusBar = newStatusBar(i18n.getString("statusBar"));
+ statusBar.addShortcutKeypress(kbF1, cmHelp,
+ i18n.getString("statusBarHelp"));
+ statusBar.addShortcutKeypress(kbF2, cmShell,
+ i18n.getString("statusBarShell"));
+ statusBar.addShortcutKeypress(kbF3, cmOpen,
+ i18n.getString("statusBarOpen"));
+ statusBar.addShortcutKeypress(kbF10, cmExit,
+ i18n.getString("statusBarExit"));
+ }
+
+ // ------------------------------------------------------------------------
+ // TWindow ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Handle window/screen resize events.
+ *
+ * @param resize resize event
+ */
+ @Override
+ public void onResize(final TResizeEvent resize) {
+ if (resize.getType() == TResizeEvent.Type.WIDGET) {
+ // Resize the treeView field
+ TResizeEvent treeSize = new TResizeEvent(TResizeEvent.Type.WIDGET,
+ resize.getWidth() - 4, resize.getHeight() - 4);
+ treeView.onResize(treeSize);
+ return;
+ }
+
+ // Pass to children instead
+ for (TWidget widget: getChildren()) {
+ widget.onResize(resize);
+ }
+ }
+
+ }
--- /dev/null
+ windowTitle=Tree View
+
+ statusBar=Treeview demonstration
+ statusBarHelp=Help
+ statusBarShell=Shell
+ statusBarOpen=Open
+ statusBarExit=Exit
--- /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 jexer.*;
+
+ /**
+ * The modified desktop.
+ */
+ public class DesktopDemo extends TDesktop {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * If true, draw the hatch. Note package private access.
+ */
+ boolean drawHatch = true;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent application
+ */
+ public DesktopDemo(final TApplication parent) {
+ super(parent);
+ }
+
+ // ------------------------------------------------------------------------
+ // TDesktop ---------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The default TDesktop draws a hatch character across everything. This
+ * version is selectable.
+ */
+ @Override
+ public void draw() {
+ if (drawHatch) {
+ super.draw();
+ }
+ }
+
+ }
--- /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.io.File;
+ import java.io.IOException;
+ import java.util.ResourceBundle;
+ import java.util.Scanner;
+
+ import jexer.TAction;
+ import jexer.TApplication;
+ import jexer.TWindow;
+ import jexer.event.TMenuEvent;
+ import jexer.menu.TMenu;
+
+ /**
+ * The demo application itself.
+ */
+ public class DesktopDemoApplication extends TApplication {
+
+ /**
+ * Translated strings.
+ */
+ private static final ResourceBundle i18n = ResourceBundle.getBundle(DesktopDemoApplication.class.getName());
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param backendType one of the TApplication.BackendType values
+ * @throws Exception if TApplication can't instantiate the Backend.
+ */
+ public DesktopDemoApplication(final BackendType backendType) throws Exception {
+ super(backendType);
+ addAllWidgets();
+ getBackend().setTitle(i18n.getString("applicationTitle"));
+ }
+
+ // ------------------------------------------------------------------------
+ // TApplication -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Handle menu events.
+ *
+ * @param menu menu event
+ * @return if true, the event was processed and should not be passed onto
+ * a window
+ */
+ @Override
+ public boolean onMenu(final TMenuEvent menu) {
+
+ if (menu.getId() == TMenu.MID_OPEN_FILE) {
+ try {
+ String filename = fileOpenBox(".");
+ if (filename != null) {
+ try {
+ File file = new File(filename);
+ StringBuilder fileContents = new StringBuilder();
+ Scanner scanner = new Scanner(file);
+ String EOL = System.getProperty("line.separator");
+
+ try {
+ while (scanner.hasNextLine()) {
+ fileContents.append(scanner.nextLine() + EOL);
+ }
+ new DemoTextWindow(this, filename,
+ fileContents.toString());
+ } finally {
+ scanner.close();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return true;
+ }
+ return super.onMenu(menu);
+ }
+
+ // ------------------------------------------------------------------------
+ // DesktopDemoApplication -------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Add all the widgets of the demo.
+ */
+ private void addAllWidgets() {
+
+ // Add the menus
+ addFileMenu();
+ addEditMenu();
+ addWindowMenu();
+ addHelpMenu();
+
+ final DesktopDemo desktop = new DesktopDemo(this);
+ setDesktop(desktop);
+
+ desktop.addButton(i18n.getString("removeHatch"), 2, 5,
+ new TAction() {
+ public void DO() {
+ desktop.drawHatch = false;
+ }
+ }
+ );
+ desktop.addButton(i18n.getString("showHatch"), 2, 8,
+ new TAction() {
+ public void DO() {
+ desktop.drawHatch = true;
+ }
+ }
+ );
+
+ final TWindow windowA = addWindow(i18n.getString("windowATitle"),
+ 25, 14);
+ final TWindow windowB = addWindow(i18n.getString("windowBTitle"),
+ 25, 14);
+ windowA.addButton(i18n.getString("showWindowB"), 2, 2,
+ new TAction() {
+ public void DO() {
+ windowB.show();
+ }
+ }
+ );
+ windowA.addButton(i18n.getString("hideWindowB"), 2, 4,
+ new TAction() {
+ public void DO() {
+ windowB.hide();
+ }
+ }
+ );
+ windowA.addButton(i18n.getString("maximizeWindowB"), 2, 6,
+ new TAction() {
+ public void DO() {
+ windowB.maximize();
+ }
+ }
+ );
+ windowA.addButton(i18n.getString("restoreWindowB"), 2, 8,
+ new TAction() {
+ public void DO() {
+ windowB.restore();
+ }
+ }
+ );
+ windowB.addButton(i18n.getString("showWindowA"), 2, 2,
+ new TAction() {
+ public void DO() {
+ windowA.show();
+ }
+ }
+ );
+ windowB.addButton(i18n.getString("hideWindowA"), 2, 4,
+ new TAction() {
+ public void DO() {
+ windowA.hide();
+ }
+ }
+ );
+ windowB.addButton(i18n.getString("maximizeWindowA"), 2, 6,
+ new TAction() {
+ public void DO() {
+ windowA.maximize();
+ }
+ }
+ );
+ windowB.addButton(i18n.getString("restoreWindowA"), 2, 8,
+ new TAction() {
+ public void DO() {
+ windowA.restore();
+ }
+ }
+ );
+
+ desktop.addButton(i18n.getString("showWindowB"), 25, 2,
+ new TAction() {
+ public void DO() {
+ windowB.show();
+ }
+ }
+ );
+ desktop.addButton(i18n.getString("hideWindowB"), 25, 5,
+ new TAction() {
+ public void DO() {
+ windowB.hide();
+ }
+ }
+ );
+ desktop.addButton(i18n.getString("showWindowA"), 25, 8,
+ new TAction() {
+ public void DO() {
+ windowA.show();
+ }
+ }
+ );
+ desktop.addButton(i18n.getString("hideWindowA"), 25, 11,
+ new TAction() {
+ public void DO() {
+ windowA.hide();
+ }
+ }
+ );
+ desktop.addButton(i18n.getString("createWindowC"), 25, 15,
+ new TAction() {
+ public void DO() {
+ final TWindow windowC = desktop.getApplication().addWindow(
+ i18n.getString("windowCTitle"), 30, 20,
+ TWindow.NOCLOSEBOX);
+ windowC.addButton(i18n.getString("closeMe"), 5, 5,
+ new TAction() {
+ public void DO() {
+ windowC.close();
+ }
+ }
+ );
+ }
+ }
+ );
+
+ desktop.addButton(i18n.getString("enableFFM"), 25, 18,
+ new TAction() {
+ public void DO() {
+ DesktopDemoApplication.this.setFocusFollowsMouse(true);
+ }
+ }
+ );
+ desktop.addButton(i18n.getString("disableFFM"), 25, 21,
+ new TAction() {
+ public void DO() {
+ DesktopDemoApplication.this.setFocusFollowsMouse(false);
+ }
+ }
+ );
+ }
+
+ }
--- /dev/null
+ applicationTitle=Demo Application
+
+ removeHatch=Remove HATCH
+ showHatch=Show HATCH
+ closeMe=Close Me
+ createWindowC=Create Window C
+ disableFFM=Disable focusFollowsMouse
+ enableFFM=Enable focusFollowsMouse
+ hideWindowA=Hide Window A
+ hideWindowB=Hide Window B
+ maximizeWindowA=Maximize Window A
+ maximizeWindowB=Maximize Window B
+ restoreWindowA=Restore Window A
+ restoreWindowB=Restore Window B
+ showWindowA=Show Window A
+ showWindowB=Show Window B
+ windowATitle=Window A
+ windowBTitle=Window B
+ windowCTitle=Window C
--- /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
+ */
+
+ /**
+ * Demonstration programs.
+ */
+ package jexer.demos;
--- /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.event;
+
+ import jexer.TCommand;
+
+ /**
+ * This class encapsulates a user command event. User commands can be
+ * generated by menu actions, keyboard accelerators, and other UI elements.
+ * Commands can operate on both the application and individual widgets.
+ */
+ public class TCommandEvent extends TInputEvent {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Command dispatched.
+ */
+ private TCommand cmd;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public contructor.
+ *
+ * @param cmd the TCommand dispatched
+ */
+ public TCommandEvent(final TCommand cmd) {
+ this.cmd = cmd;
+ }
+
+ // ------------------------------------------------------------------------
+ // TInputEvent ------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Comparison check. All fields must match to return true.
+ *
+ * @param rhs another TCommandEvent or TCommand instance
+ * @return true if all fields are equal
+ */
+ @Override
+ public boolean equals(final Object rhs) {
+ if (!(rhs instanceof TCommandEvent)
+ && !(rhs instanceof TCommand)
+ ) {
+ return false;
+ }
+
+ if (rhs instanceof TCommandEvent) {
+ TCommandEvent that = (TCommandEvent) rhs;
+ return (cmd.equals(that.cmd)
+ && (getTime().equals(that.getTime())));
+ }
+
+ TCommand that = (TCommand) rhs;
+ return (cmd.equals(that));
+ }
+
+ /**
+ * Hashcode uses all fields in equals().
+ *
+ * @return the hash
+ */
+ @Override
+ public int hashCode() {
+ int A = 13;
+ int B = 23;
+ int hash = A;
+ hash = (B * hash) + getTime().hashCode();
+ hash = (B * hash) + cmd.hashCode();
+ return hash;
+ }
+
+ /**
+ * Make human-readable description of this TCommandEvent.
+ *
+ * @return displayable String
+ */
+ @Override
+ public String toString() {
+ return String.format("CommandEvent: %s", cmd.toString());
+ }
+
+ // ------------------------------------------------------------------------
+ // TCommandEvent ----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get TCommand.
+ *
+ * @return the TCommand
+ */
+ public TCommand getCmd() {
+ return cmd;
+ }
+
+ }
--- /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.event;
+
+ import java.util.Date;
+
+ /**
+ * This is the parent class of all events dispatched to the UI.
+ */
+ public abstract class TInputEvent {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Time at which event was generated.
+ */
+ private Date time;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Protected contructor.
+ */
+ protected TInputEvent() {
+ // Save the current time
+ time = new Date();
+ }
+
+ // ------------------------------------------------------------------------
+ // TInputEvent ------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get time.
+ *
+ * @return the time that this event was generated
+ */
+ public final Date getTime() {
+ return time;
+ }
+
+ }
--- /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.event;
+
+ import jexer.TKeypress;
+
+ /**
+ * This class encapsulates a keyboard input event.
+ */
+ public class TKeypressEvent extends TInputEvent {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Keystroke received.
+ */
+ private TKeypress key;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public contructor.
+ *
+ * @param key the TKeypress received
+ */
+ public TKeypressEvent(final TKeypress key) {
+ this.key = key;
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param isKey is true, this is a function key
+ * @param fnKey the function key code (only valid if isKey is true)
+ * @param ch the character (only valid if fnKey is false)
+ * @param alt if true, ALT was pressed with this keystroke
+ * @param ctrl if true, CTRL was pressed with this keystroke
+ * @param shift if true, SHIFT was pressed with this keystroke
+ */
+ public TKeypressEvent(final boolean isKey, final int fnKey, final int ch,
+ final boolean alt, final boolean ctrl, final boolean shift) {
+
+ this.key = new TKeypress(isKey, fnKey, ch, alt, ctrl, shift);
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param key the TKeypress received
+ * @param alt if true, ALT was pressed with this keystroke
+ * @param ctrl if true, CTRL was pressed with this keystroke
+ * @param shift if true, SHIFT was pressed with this keystroke
+ */
+ public TKeypressEvent(final TKeypress key,
+ final boolean alt, final boolean ctrl, final boolean shift) {
+
+ this.key = new TKeypress(key.isFnKey(), key.getKeyCode(), key.getChar(),
+ alt, ctrl, shift);
+ }
+
+ // ------------------------------------------------------------------------
+ // TInputEvent ------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Comparison check. All fields must match to return true.
+ *
+ * @param rhs another TKeypressEvent or TKeypress instance
+ * @return true if all fields are equal
+ */
+ @Override
+ public boolean equals(final Object rhs) {
+ if (!(rhs instanceof TKeypressEvent)
+ && !(rhs instanceof TKeypress)
+ ) {
+ return false;
+ }
+
+ if (rhs instanceof TKeypressEvent) {
+ TKeypressEvent that = (TKeypressEvent) rhs;
+ return (key.equals(that.key)
+ && (getTime().equals(that.getTime())));
+ }
+
+ TKeypress that = (TKeypress) rhs;
+ return (key.equals(that));
+ }
+
+ /**
+ * Hashcode uses all fields in equals().
+ *
+ * @return the hash
+ */
+ @Override
+ public int hashCode() {
+ int A = 13;
+ int B = 23;
+ int hash = A;
+ hash = (B * hash) + getTime().hashCode();
+ hash = (B * hash) + key.hashCode();
+ return hash;
+ }
+
+ /**
+ * Make human-readable description of this TKeypressEvent.
+ *
+ * @return displayable String
+ */
+ @Override
+ public String toString() {
+ return String.format("Keypress: %s", key.toString());
+ }
+
+ // ------------------------------------------------------------------------
+ // TKeypressEvent ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get keystroke.
+ *
+ * @return keystroke
+ */
+ public TKeypress getKey() {
+ return key;
+ }
+
+ /**
+ * Create a duplicate instance.
+ *
+ * @return duplicate intance
+ */
+ public TKeypressEvent dup() {
+ TKeypressEvent keypress = new TKeypressEvent(key.dup());
+ return keypress;
+ }
+
+ }
--- /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.event;
+
+ /**
+ * This class encapsulates a menu selection event.
+ * TApplication.getMenuItem(id) can be used to obtain the TMenuItem itself,
+ * say for setting enabled/disabled/checked/etc.
+ */
+ public class TMenuEvent extends TInputEvent {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * MenuItem ID.
+ */
+ private int id;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public contructor.
+ *
+ * @param id the MenuItem ID
+ */
+ public TMenuEvent(final int id) {
+ this.id = id;
+ }
+
+ // ------------------------------------------------------------------------
+ // TInputEvent ------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Make human-readable description of this TMenuEvent.
+ *
+ * @return displayable String
+ */
+ @Override
+ public String toString() {
+ return String.format("MenuEvent: %d", id);
+ }
+
+ // ------------------------------------------------------------------------
+ // TMenuEvent -------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get the MenuItem ID.
+ *
+ * @return the ID
+ */
+ public int getId() {
+ return id;
+ }
+
+ }
--- /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.event;
+
+ /**
+ * This class encapsulates several kinds of mouse input events. Note that
+ * the relative (x,y) ARE MUTABLE: TWidget's onMouse() handlers perform that
+ * update during event dispatching.
+ */
+ public class TMouseEvent extends TInputEvent {
+
+ // ------------------------------------------------------------------------
+ // Constants --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The type of event generated.
+ */
+ public enum Type {
+ /**
+ * Mouse motion. X and Y will have screen coordinates.
+ */
+ MOUSE_MOTION,
+
+ /**
+ * Mouse button down. X and Y will have screen coordinates.
+ */
+ MOUSE_DOWN,
+
+ /**
+ * Mouse button up. X and Y will have screen coordinates.
+ */
+ MOUSE_UP,
+
+ /**
+ * Mouse double-click. X and Y will have screen coordinates.
+ */
+ MOUSE_DOUBLE_CLICK
+ }
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Type of event, one of MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN.
+ */
+ private Type type;
+
+ /**
+ * Mouse X - relative coordinates.
+ */
+ private int x;
+
+ /**
+ * Mouse Y - relative coordinates.
+ */
+ private int y;
+
+ /**
+ * Mouse X - absolute screen coordinates.
+ */
+ private int absoluteX;
+
+ /**
+ * Mouse Y - absolute screen coordinate.
+ */
+ private int absoluteY;
+
+ /**
+ * Mouse button 1 (left button).
+ */
+ private boolean mouse1;
+
+ /**
+ * Mouse button 2 (right button).
+ */
+ private boolean mouse2;
+
+ /**
+ * Mouse button 3 (middle button).
+ */
+ private boolean mouse3;
+
+ /**
+ * Mouse wheel UP (button 4).
+ */
+ private boolean mouseWheelUp;
+
+ /**
+ * Mouse wheel DOWN (button 5).
+ */
+ private boolean mouseWheelDown;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public contructor.
+ *
+ * @param type the type of event, MOUSE_MOTION, MOUSE_DOWN, or MOUSE_UP
+ * @param x relative column
+ * @param y relative row
+ * @param absoluteX absolute column
+ * @param absoluteY absolute row
+ * @param mouse1 if true, left button is down
+ * @param mouse2 if true, right button is down
+ * @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
+ */
+ 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) {
+
+ this.type = type;
+ this.x = x;
+ this.y = y;
+ this.absoluteX = absoluteX;
+ this.absoluteY = absoluteY;
+ this.mouse1 = mouse1;
+ this.mouse2 = mouse2;
+ this.mouse3 = mouse3;
+ this.mouseWheelUp = mouseWheelUp;
+ this.mouseWheelDown = mouseWheelDown;
+ }
+
+ // ------------------------------------------------------------------------
+ // TMouseEvent ------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get type.
+ *
+ * @return type
+ */
+ public Type getType() {
+ return type;
+ }
+
+ /**
+ * Get x.
+ *
+ * @return x
+ */
+ public int getX() {
+ return x;
+ }
+
+ /**
+ * Set x.
+ *
+ * @param x new relative X value
+ * @see jexer.TWidget#onMouseDown(TMouseEvent mouse)
+ * @see jexer.TWidget#onMouseDown(TMouseEvent mouse)
+ * @see jexer.TWidget#onMouseMotion(TMouseEvent mouse)
+ */
+ public void setX(final int x) {
+ this.x = x;
+ }
+
+ /**
+ * Get y.
+ *
+ * @return y
+ */
+ public int getY() {
+ return y;
+ }
+
+ /**
+ * Set y.
+ *
+ * @param y new relative Y value
+ * @see jexer.TWidget#onMouseDown(TMouseEvent mouse)
+ * @see jexer.TWidget#onMouseDown(TMouseEvent mouse)
+ * @see jexer.TWidget#onMouseMotion(TMouseEvent mouse)
+ */
+ public void setY(final int y) {
+ this.y = y;
+ }
+
+ /**
+ * Get absoluteX.
+ *
+ * @return absoluteX
+ */
+ public int getAbsoluteX() {
+ return absoluteX;
+ }
+
+ /**
+ * Set absoluteX.
+ *
+ * @param absoluteX the new value
+ */
+ public void setAbsoluteX(final int absoluteX) {
+ this.absoluteX = absoluteX;
+ }
+
+ /**
+ * Get absoluteY.
+ *
+ * @return absoluteY
+ */
+ public int getAbsoluteY() {
+ return absoluteY;
+ }
+
+ /**
+ * Set absoluteY.
+ *
+ * @param absoluteY the new value
+ */
+ public void setAbsoluteY(final int absoluteY) {
+ this.absoluteY = absoluteY;
+ }
+
+ /**
+ * Get mouse1.
+ *
+ * @return mouse1
+ */
+ public boolean isMouse1() {
+ return mouse1;
+ }
+
+ /**
+ * Get mouse2.
+ *
+ * @return mouse2
+ */
+ public boolean isMouse2() {
+ return mouse2;
+ }
+
+ /**
+ * Get mouse3.
+ *
+ * @return mouse3
+ */
+ public boolean isMouse3() {
+ return mouse3;
+ }
+
+ /**
+ * Get mouseWheelUp.
+ *
+ * @return mouseWheelUp
+ */
+ public boolean isMouseWheelUp() {
+ return mouseWheelUp;
+ }
+
+ /**
+ * Get mouseWheelDown.
+ *
+ * @return mouseWheelDown
+ */
+ public boolean isMouseWheelDown() {
+ return mouseWheelDown;
+ }
+
+ /**
+ * Create a duplicate instance.
+ *
+ * @return duplicate intance
+ */
+ public TMouseEvent dup() {
+ TMouseEvent mouse = new TMouseEvent(type, x, y, absoluteX, absoluteY,
+ mouse1, mouse2, mouse3, mouseWheelUp, mouseWheelDown);
+ return mouse;
+ }
+
+ /**
+ * Make human-readable description of this TMouseEvent.
+ *
+ * @return displayable String
+ */
+ @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",
+ type,
+ x, y,
+ absoluteX, absoluteY,
+ mouse1,
+ mouse2,
+ mouse3,
+ mouseWheelUp,
+ mouseWheelDown);
+ }
+
+ }
--- /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.event;
+
+ /**
+ * This class encapsulates a screen or window resize event.
+ */
+ public class TResizeEvent extends TInputEvent {
+
+ // ------------------------------------------------------------------------
+ // Constants --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Resize events can be generated for either a total screen resize or a
+ * widget/window resize.
+ */
+ public enum Type {
+ /**
+ * The entire screen size changed.
+ */
+ SCREEN,
+
+ /**
+ * A widget was resized.
+ */
+ WIDGET
+ }
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The type of resize.
+ */
+ private Type type;
+
+ /**
+ * New width.
+ */
+ private int width;
+
+ /**
+ * New height.
+ */
+ private int height;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public contructor.
+ *
+ * @param type the Type of resize, Screen or Widget
+ * @param width the new width
+ * @param height the new height
+ */
+ public TResizeEvent(final Type type, final int width, final int height) {
+ this.type = type;
+ this.width = width;
+ this.height = height;
+ }
+
+ // ------------------------------------------------------------------------
+ // TResizeEvent -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get resize type.
+ *
+ * @return SCREEN or WIDGET
+ */
+ public Type getType() {
+ return type;
+ }
+
+ /**
+ * Get the new width.
+ *
+ * @return width
+ */
+ public int getWidth() {
+ return width;
+ }
+
+ /**
+ * Get the new height.
+ *
+ * @return height
+ */
+ public int getHeight() {
+ return height;
+ }
+
+ /**
+ * Make human-readable description of this TResizeEvent.
+ *
+ * @return displayable String
+ */
+ @Override
+ public String toString() {
+ return String.format("Resize: %s width = %d height = %d",
+ type, width, height);
+ }
+
+ }
--- /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
+ */
+
+ /**
+ * Events that are generated by both end-user I/O (keyboard/mouse) and other
+ * UI elements (menu/resize).
+ */
+ package jexer.event;
--- /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.io;
+
+ import java.io.IOException;
+
+ /**
+ * ReadTimeoutException is thrown by TimeoutInputStream.read() when bytes are
+ * not available within the timeout specified.
+ */
+ public class ReadTimeoutException extends IOException {
+
+ /**
+ * Serializable version.
+ */
+ private static final long serialVersionUID = 1;
+
+ /**
+ * Construct an instance with a message.
+ *
+ * @param msg exception text
+ */
+ public ReadTimeoutException(String msg) {
+ super(msg);
+ }
+ }
--- /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.io;
+
+ import java.io.IOException;
+ import java.io.InputStream;
+
+ /**
+ * This class provides an optional millisecond timeout on its read()
+ * operations. This permits callers to bail out rather than block.
+ */
+ public class TimeoutInputStream extends InputStream {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The wrapped stream.
+ */
+ private InputStream stream;
+
+ /**
+ * The timeout value in millis. If it takes longer than this for bytes
+ * to be available for read then a ReadTimeoutException is thrown. A
+ * value of 0 means to block as a normal InputStream would.
+ */
+ private int timeoutMillis;
+
+ /**
+ * If true, the current read() will timeout soon.
+ */
+ private volatile boolean cancel = false;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor, at the default timeout of 10000 millis (10
+ * seconds).
+ *
+ * @param stream the wrapped InputStream
+ */
+ public TimeoutInputStream(final InputStream stream) {
+ this.stream = stream;
+ this.timeoutMillis = 10000;
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param stream the wrapped InputStream
+ * @param timeoutMillis the timeout value in millis. If it takes longer
+ * than this for bytes to be available for read then a
+ * ReadTimeoutException is thrown. A value of 0 means to block as a
+ * normal InputStream would.
+ */
+ public TimeoutInputStream(final InputStream stream,
+ final int timeoutMillis) {
+
+ if (timeoutMillis < 0) {
+ throw new IllegalArgumentException("Invalid timeoutMillis value, " +
+ "must be >= 0");
+ }
+
+ this.stream = stream;
+ this.timeoutMillis = timeoutMillis;
+ }
+
+ // ------------------------------------------------------------------------
+ // InputStream ------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Reads the next byte of data from the input stream.
+ *
+ * @return the next byte of data, or -1 if there is no more data because
+ * the end of the stream has been reached.
+ * @throws IOException if an I/O error occurs
+ */
+ @Override
+ public int read() throws IOException {
+
+ if (timeoutMillis == 0) {
+ // Block on the read().
+ return stream.read();
+ }
+
+ if (stream.available() > 0) {
+ // A byte is available now, return it.
+ return stream.read();
+ }
+
+ // We will wait up to timeoutMillis to see if a byte is available.
+ // If not, we throw ReadTimeoutException.
+ long checkTime = System.currentTimeMillis();
+ while (stream.available() == 0) {
+ long now = System.currentTimeMillis();
+ synchronized (this) {
+ if ((now - checkTime > timeoutMillis) || (cancel == true)) {
+ if (cancel == true) {
+ cancel = false;
+ }
+ throw new ReadTimeoutException("Timeout on read(): " +
+ (int) (now - checkTime) + " millis and still no data");
+ }
+ }
+ try {
+ // How long do we sleep for, eh? For now we will go with 2
+ // millis.
+ Thread.sleep(2);
+ } catch (InterruptedException e) {
+ // SQUASH
+ }
+ }
+
+ if (stream.available() > 0) {
+ // A byte is available now, return it.
+ return stream.read();
+ }
+
+ throw new IOException("InputStream claimed a byte was available, but " +
+ "now it is not. What is going on?");
+ }
+
+ /**
+ * Reads some number of bytes from the input stream and stores them into
+ * the buffer array b.
+ *
+ * @param b the buffer into which the data is read.
+ * @return the total number of bytes read into the buffer, or -1 if there
+ * is no more data because the end of the stream has been reached.
+ * @throws IOException if an I/O error occurs
+ */
+ @Override
+ public int read(final byte[] b) throws IOException {
+ if (timeoutMillis == 0) {
+ // Block on the read().
+ return stream.read(b);
+ }
+
+ int remaining = b.length;
+
+ if (stream.available() >= remaining) {
+ // Enough bytes are available now, return them.
+ return stream.read(b);
+ }
+
+ while (remaining > 0) {
+
+ // We will wait up to timeoutMillis to see if a byte is
+ // available. If not, we throw ReadTimeoutException.
+ long checkTime = System.currentTimeMillis();
+ while (stream.available() == 0) {
+ if (remaining > 0) {
+ return (b.length - remaining);
+ }
+
+ long now = System.currentTimeMillis();
+ synchronized (this) {
+ if ((now - checkTime > timeoutMillis) || (cancel == true)) {
+ if (cancel == true) {
+ cancel = false;
+ }
+ throw new ReadTimeoutException("Timeout on read(): " +
+ (int) (now - checkTime) + " millis and still no " +
+ "data");
+ }
+ }
+ try {
+ // How long do we sleep for, eh? For now we will go with
+ // 2 millis.
+ Thread.sleep(2);
+ } catch (InterruptedException e) {
+ // SQUASH
+ }
+ }
+
+ if (stream.available() > 0) {
+ // At least one byte is available now, read it.
+ int n = stream.available();
+ if (remaining < n) {
+ n = remaining;
+ }
+ int rc = stream.read(b, b.length - remaining, n);
+ if (rc == -1) {
+ // This shouldn't happen.
+ throw new IOException("InputStream claimed bytes were " +
+ "available, but read() returned -1. What is going " +
+ "on?");
+ }
+ remaining -= rc;
+ if (remaining == 0) {
+ return b.length;
+ }
+ }
+ }
+
+ throw new IOException("InputStream claimed all bytes were available, " +
+ "but now it is not. What is going on?");
+ }
+
+ /**
+ * Reads up to len bytes of data from the input stream into an array of
+ * bytes.
+ *
+ * @param b the buffer into which the data is read.
+ * @param off the start offset in array b at which the data is written.
+ * @param len the maximum number of bytes to read.
+ * @return the total number of bytes read into the buffer, or -1 if there
+ * is no more data because the end of the stream has been reached.
+ * @throws IOException if an I/O error occurs
+ */
+ @Override
+ public int read(final byte[] b, final int off,
+ final int len) throws IOException {
+
+ if (timeoutMillis == 0) {
+ // Block on the read().
+ return stream.read(b);
+ }
+
+ int remaining = len;
+
+ if (stream.available() >= remaining) {
+ // Enough bytes are available now, return them.
+ return stream.read(b, off, remaining);
+ }
+
+ while (remaining > 0) {
+
+ // We will wait up to timeoutMillis to see if a byte is
+ // available. If not, we throw ReadTimeoutException.
+ long checkTime = System.currentTimeMillis();
+ while (stream.available() == 0) {
+ if (remaining > 0) {
+ return (len - remaining);
+ }
+
+ long now = System.currentTimeMillis();
+ synchronized (this) {
+ if ((now - checkTime > timeoutMillis) || (cancel == true)) {
+ if (cancel == true) {
+ cancel = false;
+ }
+ throw new ReadTimeoutException("Timeout on read(): " +
+ (int) (now - checkTime) + " millis and still no " +
+ "data");
+ }
+ }
+ try {
+ // How long do we sleep for, eh? For now we will go with
+ // 2 millis.
+ Thread.sleep(2);
+ } catch (InterruptedException e) {
+ // SQUASH
+ }
+ }
+
+ if (stream.available() > 0) {
+ // At least one byte is available now, read it.
+ int n = stream.available();
+ if (remaining < n) {
+ n = remaining;
+ }
+ int rc = stream.read(b, off + len - remaining, n);
+ if (rc == -1) {
+ // This shouldn't happen.
+ throw new IOException("InputStream claimed bytes were " +
+ "available, but read() returned -1. What is going " +
+ "on?");
+ }
+ remaining -= rc;
+ if (remaining == 0) {
+ return len;
+ }
+ }
+ }
+
+ throw new IOException("InputStream claimed all bytes were available, " +
+ "but now it is not. What is going on?");
+ }
+
+ /**
+ * Returns an estimate of the number of bytes that can be read (or
+ * skipped over) from this input stream without blocking by the next
+ * invocation of a method for this input stream.
+ *
+ * @return an estimate of the number of bytes that can be read (or
+ * skipped over) from this input stream without blocking or 0 when it
+ * reaches the end of the input stream.
+ * @throws IOException if an I/O error occurs
+ */
+ @Override
+ public int available() throws IOException {
+ return stream.available();
+ }
+
+ /**
+ * Closes this input stream and releases any system resources associated
+ * with the stream.
+ *
+ * @throws IOException if an I/O error occurs
+ */
+ @Override
+ public void close() throws IOException {
+ stream.close();
+ }
+
+ /**
+ * Marks the current position in this input stream.
+ *
+ * @param readLimit the maximum limit of bytes that can be read before
+ * the mark position becomes invalid
+ */
+ @Override
+ public void mark(final int readLimit) {
+ stream.mark(readLimit);
+ }
+
+ /**
+ * Tests if this input stream supports the mark and reset methods.
+ *
+ * @return true if this stream instance supports the mark and reset
+ * methods; false otherwise
+ */
+ @Override
+ public boolean markSupported() {
+ return stream.markSupported();
+ }
+
+ /**
+ * Repositions this stream to the position at the time the mark method
+ * was last called on this input stream.
+ *
+ * @throws IOException if an I/O error occurs
+ */
+ @Override
+ public void reset() throws IOException {
+ stream.reset();
+ }
+
+ /**
+ * Skips over and discards n bytes of data from this input stream.
+ *
+ * @param n the number of bytes to be skipped
+ * @return the actual number of bytes skipped
+ * @throws IOException if an I/O error occurs
+ */
+ @Override
+ public long skip(final long n) throws IOException {
+ return stream.skip(n);
+ }
+
+ // ------------------------------------------------------------------------
+ // TimeoutInputStream -----------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Request that the current read() operation timeout immediately.
+ */
+ public synchronized void cancelRead() {
+ cancel = 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
+ */
+
+ /**
+ * java.io subclasses.
+ */
+ package jexer.io;
--- /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.layout;
+
+ import java.util.ArrayList;
+
+ import jexer.TWidget;
+ import jexer.event.TResizeEvent;
+
+ /**
+ * BoxLayoutManager repositions child widgets based on the order they are
+ * added to the parent widget and desired orientation.
+ */
+ public class BoxLayoutManager implements LayoutManager {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * If true, orient vertically. If false, orient horizontally.
+ */
+ private boolean vertical = true;
+
+ /**
+ * Current width.
+ */
+ private int width = 0;
+
+ /**
+ * Current height.
+ */
+ private int height = 0;
+
+ /**
+ * Widgets being managed.
+ */
+ private ArrayList<TWidget> children = new ArrayList<TWidget>();
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param width the width of the parent widget
+ * @param height the height of the parent widget
+ * @param vertical if true, arrange widgets vertically
+ */
+ public BoxLayoutManager(final int width, final int height,
+ final boolean vertical) {
+
+ this.width = width;
+ this.height = height;
+ this.vertical = vertical;
+ }
+
+ // ------------------------------------------------------------------------
+ // LayoutManager ----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Process the parent widget's resize event, and resize/reposition child
+ * widgets.
+ *
+ * @param resize resize event
+ */
+ public void onResize(final TResizeEvent resize) {
+ if (resize.getType() == TResizeEvent.Type.WIDGET) {
+ width = resize.getWidth();
+ height = resize.getHeight();
+ layoutChildren();
+ }
+ }
+
+ /**
+ * Add a child widget to manage.
+ *
+ * @param child the widget to manage
+ */
+ public void add(final TWidget child) {
+ children.add(child);
+ layoutChildren();
+ }
+
+ /**
+ * Remove a child widget from those managed by this LayoutManager.
+ *
+ * @param child the widget to remove
+ */
+ public void remove(final TWidget child) {
+ children.remove(child);
+ layoutChildren();
+ }
+
+ /**
+ * Reset a child widget's original/preferred size.
+ *
+ * @param child the widget to manage
+ */
+ public void resetSize(final TWidget child) {
+ // NOP
+ }
+
+ // ------------------------------------------------------------------------
+ // BoxLayoutManager -------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Resize/reposition child widgets based on horizontal/vertical
+ * arrangement.
+ */
+ private void layoutChildren() {
+ if (children.size() == 0) {
+ return;
+ }
+ if (vertical) {
+ int widgetHeight = Math.max(1, height / children.size());
+ int leftoverHeight = height % children.size();
+ for (int i = 0; i < children.size() - 1; i++) {
+ TWidget child = children.get(i);
+ child.setDimensions(child.getX(), i * widgetHeight,
+ width, widgetHeight);
+ }
+ TWidget child = children.get(children.size() - 1);
+ child.setDimensions(child.getX(),
+ (children.size() - 1) * widgetHeight, width,
+ widgetHeight + leftoverHeight);
+ } else {
+ int widgetWidth = Math.max(1, width / children.size());
+ int leftoverWidth = width % children.size();
+ for (int i = 0; i < children.size() - 1; i++) {
+ TWidget child = children.get(i);
+ child.setDimensions(i * widgetWidth, child.getY(),
+ widgetWidth, height);
+ }
+ TWidget child = children.get(children.size() - 1);
+ child.setDimensions((children.size() - 1) * widgetWidth,
+ child.getY(), widgetWidth + leftoverWidth, height);
+ }
+ }
+
+ }
--- /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.layout;
+
+ import jexer.TWidget;
+ import jexer.event.TResizeEvent;
+
+ /**
+ * A LayoutManager provides automatic positioning and sizing of a TWidget's
+ * child TWidgets.
+ */
+ public interface LayoutManager {
+
+ /**
+ * Process the parent widget's resize event, and resize/reposition child
+ * widgets.
+ *
+ * @param resize resize event
+ */
+ public void onResize(final TResizeEvent resize);
+
+ /**
+ * Add a child widget to manage.
+ *
+ * @param child the widget to manage
+ */
+ public void add(final TWidget child);
+
+ /**
+ * Remove a child widget from those managed by this LayoutManager.
+ *
+ * @param child the widget to remove
+ */
+ public void remove(final TWidget child);
+
+ /**
+ * Reset a child widget's original/preferred size.
+ *
+ * @param child the widget to manage
+ */
+ public void resetSize(final TWidget child);
+
+ }
--- /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.layout;
+
+ import java.awt.Rectangle;
+ import java.util.HashMap;
+
+ import jexer.TWidget;
+ import jexer.event.TResizeEvent;
+
+ /**
+ * StretchLayoutManager repositions child widgets based on their coordinates
+ * when added and the current widget size.
+ */
+ public class StretchLayoutManager implements LayoutManager {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Current width.
+ */
+ private int width = 0;
+
+ /**
+ * Current height.
+ */
+ private int height = 0;
+
+ /**
+ * Original width.
+ */
+ private int originalWidth = 0;
+
+ /**
+ * Original height.
+ */
+ private int originalHeight = 0;
+
+ /**
+ * Map of widget to original dimensions.
+ */
+ private HashMap<TWidget, Rectangle> children = new HashMap<TWidget, Rectangle>();
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param width the width of the parent widget
+ * @param height the height of the parent widget
+ */
+ public StretchLayoutManager(final int width, final int height) {
+ originalWidth = width;
+ originalHeight = height;
+ this.width = width;
+ this.height = height;
+ }
+
+ // ------------------------------------------------------------------------
+ // LayoutManager ----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Process the parent widget's resize event, and resize/reposition child
+ * widgets.
+ *
+ * @param resize resize event
+ */
+ public void onResize(final TResizeEvent resize) {
+ if (resize.getType() == TResizeEvent.Type.WIDGET) {
+ width = resize.getWidth();
+ height = resize.getHeight();
+ layoutChildren();
+ }
+ }
+
+ /**
+ * Add a child widget to manage.
+ *
+ * @param child the widget to manage
+ */
+ public void add(final TWidget child) {
+ Rectangle rect = new Rectangle(child.getX(), child.getY(),
+ child.getWidth(), child.getHeight());
+ children.put(child, rect);
+ layoutChildren();
+ }
+
+ /**
+ * Remove a child widget from those managed by this LayoutManager.
+ *
+ * @param child the widget to remove
+ */
+ public void remove(final TWidget child) {
+ children.remove(child);
+ layoutChildren();
+ }
+
+ /**
+ * Reset a child widget's original/preferred size.
+ *
+ * @param child the widget to manage
+ */
+ public void resetSize(final TWidget child) {
+ // For this layout, adding is the same as replacing.
+ add(child);
+ }
+
+ // ------------------------------------------------------------------------
+ // StretchLayoutManager ---------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Resize/reposition child widgets based on difference between current
+ * dimensions and the original dimensions.
+ */
+ private void layoutChildren() {
+ double widthRatio = (double) width / originalWidth;
+ if (!Double.isFinite(widthRatio)) {
+ widthRatio = 1;
+ }
+ double heightRatio = (double) height / originalHeight;
+ if (!Double.isFinite(heightRatio)) {
+ heightRatio = 1;
+ }
+ for (TWidget child: children.keySet()) {
+ Rectangle rect = children.get(child);
+ child.setDimensions((int) (rect.getX() * widthRatio),
+ (int) (rect.getY() * heightRatio),
+ (int) (rect.getWidth() * widthRatio),
+ (int) (rect.getHeight() * heightRatio));
+ }
+ }
+
+ }
--- /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
+ */
+
+ /**
+ * Available layout managers.
+ */
+ package jexer.layout;
--- /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.menu;
+
+ import java.util.ResourceBundle;
+
+ import jexer.TApplication;
+ import jexer.TKeypress;
+ import jexer.TWidget;
+ import jexer.TWindow;
+ import jexer.bits.CellAttributes;
+ import jexer.bits.GraphicsChars;
+ import jexer.bits.MnemonicString;
+ import jexer.bits.StringUtils;
+ import jexer.event.TKeypressEvent;
+ import jexer.event.TMouseEvent;
+ import static jexer.TKeypress.*;
+
+ /**
+ * TMenu is a top-level collection of TMenuItems.
+ */
+ public class TMenu extends TWindow {
+
+ /**
+ * Translated strings.
+ */
+ private static final ResourceBundle i18n = ResourceBundle.getBundle(TMenu.class.getName());
+
+ // ------------------------------------------------------------------------
+ // Constants --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ // Reserved menu item IDs
+ public static final int MID_UNUSED = -1;
+
+ // Tools menu
+ public static final int MID_REPAINT = 1;
+ public static final int MID_VIEW_IMAGE = 2;
+ public static final int MID_SCREEN_OPTIONS = 3;
+
+ // File menu
+ public static final int MID_NEW = 10;
+ public static final int MID_EXIT = 11;
+ public static final int MID_QUIT = MID_EXIT;
+ public static final int MID_OPEN_FILE = 12;
+ 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;
+
+ // Search menu
+ public static final int MID_FIND = 30;
+ public static final int MID_REPLACE = 31;
+ public static final int MID_SEARCH_AGAIN = 32;
+ public static final int MID_GOTO_LINE = 33;
+
+ // Window menu
+ public static final int MID_TILE = 40;
+ public static final int MID_CASCADE = 41;
+ public static final int MID_CLOSE_ALL = 42;
+ public static final int MID_WINDOW_MOVE = 43;
+ public static final int MID_WINDOW_ZOOM = 44;
+ public static final int MID_WINDOW_NEXT = 45;
+ public static final int MID_WINDOW_PREVIOUS = 46;
+ public static final int MID_WINDOW_CLOSE = 47;
+
+ // Help menu
+ public static final int MID_HELP_CONTENTS = 50;
+ public static final int MID_HELP_INDEX = 51;
+ public static final int MID_HELP_SEARCH = 52;
+ public static final int MID_HELP_PREVIOUS = 53;
+ public static final int MID_HELP_HELP = 54;
+ public static final int MID_HELP_ACTIVE_FILE = 55;
+ public static final int MID_ABOUT = 56;
+
+ // Table menu
+ public static final int MID_TABLE_RENAME_ROW = 60;
+ public static final int MID_TABLE_RENAME_COLUMN = 61;
+ public static final int MID_TABLE_VIEW_ROW_LABELS = 70;
+ public static final int MID_TABLE_VIEW_COLUMN_LABELS = 71;
+ public static final int MID_TABLE_VIEW_HIGHLIGHT_ROW = 72;
+ public static final int MID_TABLE_VIEW_HIGHLIGHT_COLUMN = 73;
+ public static final int MID_TABLE_BORDER_NONE = 80;
+ public static final int MID_TABLE_BORDER_ALL = 81;
+ public static final int MID_TABLE_BORDER_CELL_NONE = 82;
+ public static final int MID_TABLE_BORDER_CELL_ALL = 83;
+ public static final int MID_TABLE_BORDER_RIGHT = 84;
+ public static final int MID_TABLE_BORDER_LEFT = 85;
+ public static final int MID_TABLE_BORDER_TOP = 86;
+ public static final int MID_TABLE_BORDER_BOTTOM = 87;
+ public static final int MID_TABLE_BORDER_DOUBLE_BOTTOM = 88;
+ public static final int MID_TABLE_BORDER_THICK_BOTTOM = 89;
+ public static final int MID_TABLE_DELETE_LEFT = 100;
+ public static final int MID_TABLE_DELETE_UP = 101;
+ public static final int MID_TABLE_DELETE_ROW = 102;
+ public static final int MID_TABLE_DELETE_COLUMN = 103;
+ public static final int MID_TABLE_INSERT_LEFT = 104;
+ public static final int MID_TABLE_INSERT_RIGHT = 105;
+ public static final int MID_TABLE_INSERT_ABOVE = 106;
+ public static final int MID_TABLE_INSERT_BELOW = 107;
+ public static final int MID_TABLE_COLUMN_NARROW = 110;
+ public static final int MID_TABLE_COLUMN_WIDEN = 111;
+ public static final int MID_TABLE_FILE_OPEN_CSV = 115;
+ public static final int MID_TABLE_FILE_SAVE_CSV = 116;
+ public static final int MID_TABLE_FILE_SAVE_TEXT = 117;
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * If true, this is a sub-menu. Note package private access.
+ */
+ boolean isSubMenu = false;
+
+ /**
+ * The X position of the menu's title.
+ */
+ private int titleX;
+
+ /**
+ * The shortcut and title.
+ */
+ private MnemonicString mnemonic;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent application
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param label mnemonic menu title. Label must contain a keyboard
+ * shortcut (mnemonic), denoted by prefixing a letter with "&",
+ * e.g. "&File"
+ */
+ public TMenu(final TApplication parent, final int x, final int y,
+ final String label) {
+
+ super(parent, label, x, y, parent.getScreen().getWidth(),
+ parent.getScreen().getHeight());
+
+ // Setup the menu shortcut
+ mnemonic = new MnemonicString(label);
+ setTitle(mnemonic.getRawLabel());
+ assert (mnemonic.getShortcutIdx() >= 0);
+
+ // Recompute width and height to reflect an empty menu
+ setWidth(StringUtils.width(getTitle()) + 4);
+ setHeight(2);
+
+ setActive(false);
+ }
+
+ // ------------------------------------------------------------------------
+ // Event handlers ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Handle mouse button presses.
+ *
+ * @param mouse mouse button event
+ */
+ @Override
+ public void onMouseDown(final TMouseEvent mouse) {
+ this.mouse = mouse;
+ super.onMouseDown(mouse);
+
+ // Pass to children
+ for (TWidget widget: getChildren()) {
+ if (widget.mouseWouldHit(mouse)) {
+ // Dispatch to this child, also activate it
+ activate(widget);
+
+ // Set x and y relative to the child's coordinates
+ mouse.setX(mouse.getAbsoluteX() - widget.getAbsoluteX());
+ mouse.setY(mouse.getAbsoluteY() - widget.getAbsoluteY());
+ widget.handleEvent(mouse);
+ return;
+ }
+ }
+ }
+
+ /**
+ * Handle mouse button releases.
+ *
+ * @param mouse mouse button release event
+ */
+ @Override
+ public void onMouseUp(final TMouseEvent mouse) {
+ this.mouse = mouse;
+
+ // Pass to children
+ for (TWidget widget: getChildren()) {
+ if (widget.mouseWouldHit(mouse)) {
+ // Dispatch to this child, also activate it
+ activate(widget);
+
+ // Set x and y relative to the child's coordinates
+ mouse.setX(mouse.getAbsoluteX() - widget.getAbsoluteX());
+ mouse.setY(mouse.getAbsoluteY() - widget.getAbsoluteY());
+ widget.handleEvent(mouse);
+ return;
+ }
+ }
+ }
+
+ /**
+ * Handle mouse movements.
+ *
+ * @param mouse mouse motion event
+ */
+ @Override
+ public void onMouseMotion(final TMouseEvent mouse) {
+ this.mouse = mouse;
+
+ // See if we should activate a different menu item
+ for (TWidget widget: getChildren()) {
+ if ((mouse.isMouse1())
+ && (widget.mouseWouldHit(mouse))
+ ) {
+ // Activate this menu item
+ activate(widget);
+ if (widget instanceof TSubMenu) {
+ ((TSubMenu) widget).dispatch();
+ }
+ return;
+ }
+ }
+ }
+
+ /**
+ * Handle keystrokes.
+ *
+ * @param keypress keystroke event
+ */
+ @Override
+ public void onKeypress(final TKeypressEvent keypress) {
+
+ /*
+ System.err.printf("keypress: %s active child: %s\n", keypress,
+ getActiveChild());
+ */
+
+ if (getActiveChild() != this) {
+ if (getActiveChild() instanceof TMenu) {
+ getActiveChild().onKeypress(keypress);
+ return;
+ }
+
+ if (getActiveChild() instanceof TSubMenu) {
+ TSubMenu subMenu = (TSubMenu) getActiveChild();
+ if (subMenu.menu.isActive()) {
+ subMenu.onKeypress(keypress);
+ return;
+ }
+ }
+ }
+
+ if (keypress.equals(kbEsc)) {
+ getApplication().closeMenu();
+ return;
+ }
+ if (keypress.equals(kbDown)) {
+ switchWidget(true);
+ return;
+ }
+ if (keypress.equals(kbUp)) {
+ switchWidget(false);
+ return;
+ }
+ if (keypress.equals(kbRight)) {
+ getApplication().switchMenu(true);
+ return;
+ }
+ if (keypress.equals(kbLeft)) {
+ if (isSubMenu) {
+ getApplication().closeSubMenu();
+ } else {
+ getApplication().switchMenu(false);
+ }
+ return;
+ }
+
+ // Switch to a menuItem if it has an mnemonic
+ if (!keypress.getKey().isFnKey()
+ && !keypress.getKey().isAlt()
+ && !keypress.getKey().isCtrl()) {
+
+ // System.err.println("Checking children for mnemonic...");
+
+ for (TWidget widget: getChildren()) {
+ TMenuItem item = (TMenuItem) widget;
+ if ((item.isEnabled() == true)
+ && (item.getMnemonic() != null)
+ && (Character.toLowerCase(item.getMnemonic().getShortcut())
+ == Character.toLowerCase(keypress.getKey().getChar()))
+ ) {
+ // System.err.println("activate: " + item);
+
+ // Send an enter keystroke to it
+ activate(item);
+ item.handleEvent(new TKeypressEvent(kbEnter));
+ return;
+ }
+ }
+ }
+
+ // Dispatch the keypress to an active widget
+ for (TWidget widget: getChildren()) {
+ if (widget.isActive()) {
+ widget.handleEvent(keypress);
+ return;
+ }
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // TWindow ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Draw a top-level menu with title and menu items.
+ */
+ @Override
+ public void draw() {
+ CellAttributes background = getTheme().getColor("tmenu");
+
+ assert (isAbsoluteActive());
+
+ // Fill in the interior background
+ for (int i = 0; i < getHeight(); i++) {
+ hLineXY(0, i, getWidth(), ' ', background);
+ }
+
+ // Draw the box
+ char cTopLeft;
+ char cTopRight;
+ char cBottomLeft;
+ char cBottomRight;
+ char cHSide;
+
+ cTopLeft = GraphicsChars.ULCORNER;
+ cTopRight = GraphicsChars.URCORNER;
+ cBottomLeft = GraphicsChars.LLCORNER;
+ cBottomRight = GraphicsChars.LRCORNER;
+ cHSide = GraphicsChars.SINGLE_BAR;
+
+ // Place the corner characters
+ putCharXY(1, 0, cTopLeft, background);
+ putCharXY(getWidth() - 2, 0, cTopRight, background);
+ putCharXY(1, getHeight() - 1, cBottomLeft, background);
+ putCharXY(getWidth() - 2, getHeight() - 1, cBottomRight, background);
+
+ // Draw the box lines
+ hLineXY(1 + 1, 0, getWidth() - 4, cHSide, background);
+ hLineXY(1 + 1, getHeight() - 1, getWidth() - 4, cHSide, background);
+
+ // Draw a shadow
+ drawBoxShadow(0, 0, getWidth(), getHeight());
+ }
+
+ // ------------------------------------------------------------------------
+ // TMenu ------------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Set the menu title X position.
+ *
+ * @param titleX the position
+ */
+ public void setTitleX(final int titleX) {
+ this.titleX = titleX;
+ }
+
+ /**
+ * Get the menu title X position.
+ *
+ * @return the position
+ */
+ public int getTitleX() {
+ return titleX;
+ }
+
+ /**
+ * Get the mnemonic string.
+ *
+ * @return the full mnemonic string
+ */
+ public MnemonicString getMnemonic() {
+ return mnemonic;
+ }
+
+ /**
+ * Convenience function to add a menu item.
+ *
+ * @param id menu item ID. Must be greater than 1024.
+ * @param label menu item label
+ * @return the new menu item
+ */
+ public TMenuItem addItem(final int id, final String label) {
+ return addItemInternal(id, label, null);
+ }
+
+ /**
+ * 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) {
+
+ assert (id >= 1024);
+ return addItemInternal(id, label, null, enabled);
+ }
+
+ /**
+ * 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
+ * @return the new menu item
+ */
+ public TMenuItem addItem(final int id, final String label,
+ final TKeypress key) {
+
+ assert (id >= 1024);
+ return addItemInternal(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) {
+
+ TMenuItem item = addItem(id, label, key);
+ item.setEnabled(enabled);
+ return item;
+ }
+
+ /**
+ * 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
+ * @return the new menu item
+ */
+ private TMenuItem addItemInternal(final int id, final String label,
+ final TKeypress key) {
+
+ return addItemInternal(id, label, key, true);
+ }
+
+ /**
+ * 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
+ */
+ private TMenuItem addItemInternal(final int id, final String label,
+ final TKeypress key, final boolean enabled) {
+
+ int newY = getChildren().size() + 1;
+ assert (newY < getHeight());
+
+ TMenuItem menuItem = new TMenuItem(this, id, 1, newY, label);
+ menuItem.setKey(key);
+ menuItem.setEnabled(enabled);
+ setHeight(getHeight() + 1);
+ if (menuItem.getWidth() + 2 > getWidth()) {
+ setWidth(menuItem.getWidth() + 2);
+ }
+ for (TWidget widget: getChildren()) {
+ widget.setWidth(getWidth() - 2);
+ }
+ getApplication().addMenuItem(menuItem);
+ getApplication().recomputeMenuX();
+ activate(0);
+ return menuItem;
+ }
+
+ /**
+ * Convenience function to add one of the default menu items.
+ *
+ * @param id menu item ID. Must be between 0 (inclusive) and 1023
+ * (inclusive).
+ * @return the new menu item
+ */
+ public TMenuItem addDefaultItem(final int id) {
+ return addDefaultItem(id, true);
+ }
+
+ /**
+ * Convenience function to add one of the default menu items.
+ *
+ * @param id menu item ID. Must be between 0 (inclusive) and 1023
+ * (inclusive).
+ * @param enabled default state for enabled
+ * @return the new menu item
+ */
+ public TMenuItem addDefaultItem(final int id, final boolean enabled) {
+ assert (id >= 0);
+ assert (id < 1024);
+
+ String label;
+ TKeypress key = null;
+ boolean checkable = false;
+ boolean checked = false;
+
+ switch (id) {
+
+ case MID_REPAINT:
+ label = i18n.getString("menuRepaintDesktop");
+ break;
+
+ case MID_VIEW_IMAGE:
+ label = i18n.getString("menuViewImage");
+ break;
+
+ case MID_SCREEN_OPTIONS:
+ label = i18n.getString("menuScreenOptions");
+ break;
+
+ case MID_NEW:
+ label = i18n.getString("menuNew");
+ break;
+
+ case MID_EXIT:
+ label = i18n.getString("menuExit");
+ key = kbAltX;
+ break;
+
+ case MID_SHELL:
+ label = i18n.getString("menuShell");
+ break;
+
+ case MID_OPEN_FILE:
+ label = i18n.getString("menuOpen");
+ key = kbF3;
+ break;
+
+ case MID_CUT:
+ label = i18n.getString("menuCut");
+ key = kbCtrlX;
+ break;
+ case MID_COPY:
+ label = i18n.getString("menuCopy");
+ key = kbCtrlC;
+ break;
+ case MID_PASTE:
+ label = i18n.getString("menuPaste");
+ key = kbCtrlV;
+ break;
+ case MID_CLEAR:
+ label = i18n.getString("menuClear");
+ // key = kbDel;
+ break;
+
+ case MID_FIND:
+ label = i18n.getString("menuFind");
+ break;
+ case MID_REPLACE:
+ label = i18n.getString("menuReplace");
+ break;
+ case MID_SEARCH_AGAIN:
+ label = i18n.getString("menuSearchAgain");
+ key = kbCtrlL;
+ break;
+ case MID_GOTO_LINE:
+ label = i18n.getString("menuGotoLine");
+ break;
+
+ case MID_TILE:
+ label = i18n.getString("menuWindowTile");
+ break;
+ case MID_CASCADE:
+ label = i18n.getString("menuWindowCascade");
+ break;
+ case MID_CLOSE_ALL:
+ label = i18n.getString("menuWindowCloseAll");
+ break;
+ case MID_WINDOW_MOVE:
+ label = i18n.getString("menuWindowMove");
+ key = kbCtrlF5;
+ break;
+ case MID_WINDOW_ZOOM:
+ label = i18n.getString("menuWindowZoom");
+ key = kbF5;
+ break;
+ case MID_WINDOW_NEXT:
+ label = i18n.getString("menuWindowNext");
+ key = kbF6;
+ break;
+ case MID_WINDOW_PREVIOUS:
+ label = i18n.getString("menuWindowPrevious");
+ key = kbShiftF6;
+ break;
+ case MID_WINDOW_CLOSE:
+ label = i18n.getString("menuWindowClose");
+ key = kbCtrlW;
+ break;
+
+ case MID_HELP_CONTENTS:
+ label = i18n.getString("menuHelpContents");
+ break;
+ case MID_HELP_INDEX:
+ label = i18n.getString("menuHelpIndex");
+ key = kbShiftF1;
+ break;
+ case MID_HELP_SEARCH:
+ label = i18n.getString("menuHelpSearch");
+ key = kbCtrlF1;
+ break;
+ case MID_HELP_PREVIOUS:
+ label = i18n.getString("menuHelpPrevious");
+ key = kbAltF1;
+ break;
+ case MID_HELP_HELP:
+ label = i18n.getString("menuHelpHelp");
+ break;
+ case MID_HELP_ACTIVE_FILE:
+ label = i18n.getString("menuHelpActive");
+ break;
+ case MID_ABOUT:
+ label = i18n.getString("menuHelpAbout");
+ break;
+
+ case MID_TABLE_RENAME_COLUMN:
+ label = i18n.getString("menuTableRenameColumn");
+ break;
+ case MID_TABLE_RENAME_ROW:
+ label = i18n.getString("menuTableRenameRow");
+ break;
+ case MID_TABLE_VIEW_ROW_LABELS:
+ label = i18n.getString("menuTableViewRowLabels");
+ checkable = true;
+ checked = true;
+ break;
+ case MID_TABLE_VIEW_COLUMN_LABELS:
+ label = i18n.getString("menuTableViewColumnLabels");
+ checkable = true;
+ checked = true;
+ break;
+ case MID_TABLE_VIEW_HIGHLIGHT_ROW:
+ label = i18n.getString("menuTableViewHighlightRow");
+ checkable = true;
+ checked = true;
+ break;
+ case MID_TABLE_VIEW_HIGHLIGHT_COLUMN:
+ label = i18n.getString("menuTableViewHighlightColumn");
+ checkable = true;
+ checked = true;
+ break;
+
+ case MID_TABLE_BORDER_NONE:
+ label = i18n.getString("menuTableBorderNone");
+ break;
+ case MID_TABLE_BORDER_ALL:
+ label = i18n.getString("menuTableBorderAll");
+ break;
+ case MID_TABLE_BORDER_CELL_NONE:
+ label = i18n.getString("menuTableBorderCellNone");
+ break;
+ case MID_TABLE_BORDER_CELL_ALL:
+ label = i18n.getString("menuTableBorderCellAll");
+ break;
+ case MID_TABLE_BORDER_RIGHT:
+ label = i18n.getString("menuTableBorderRight");
+ break;
+ case MID_TABLE_BORDER_LEFT:
+ label = i18n.getString("menuTableBorderLeft");
+ break;
+ case MID_TABLE_BORDER_TOP:
+ label = i18n.getString("menuTableBorderTop");
+ break;
+ case MID_TABLE_BORDER_BOTTOM:
+ label = i18n.getString("menuTableBorderBottom");
+ break;
+ case MID_TABLE_BORDER_DOUBLE_BOTTOM:
+ label = i18n.getString("menuTableBorderDoubleBottom");
+ break;
+ case MID_TABLE_BORDER_THICK_BOTTOM:
+ label = i18n.getString("menuTableBorderThickBottom");
+ break;
+ case MID_TABLE_DELETE_LEFT:
+ label = i18n.getString("menuTableDeleteLeft");
+ break;
+ case MID_TABLE_DELETE_UP:
+ label = i18n.getString("menuTableDeleteUp");
+ break;
+ case MID_TABLE_DELETE_ROW:
+ label = i18n.getString("menuTableDeleteRow");
+ break;
+ case MID_TABLE_DELETE_COLUMN:
+ label = i18n.getString("menuTableDeleteColumn");
+ break;
+ case MID_TABLE_INSERT_LEFT:
+ label = i18n.getString("menuTableInsertLeft");
+ break;
+ case MID_TABLE_INSERT_RIGHT:
+ label = i18n.getString("menuTableInsertRight");
+ break;
+ case MID_TABLE_INSERT_ABOVE:
+ label = i18n.getString("menuTableInsertAbove");
+ break;
+ case MID_TABLE_INSERT_BELOW:
+ label = i18n.getString("menuTableInsertBelow");
+ break;
+ case MID_TABLE_COLUMN_NARROW:
+ label = i18n.getString("menuTableColumnNarrow");
+ key = kbShiftLeft;
+ break;
+ case MID_TABLE_COLUMN_WIDEN:
+ label = i18n.getString("menuTableColumnWiden");
+ key = kbShiftRight;
+ break;
+ case MID_TABLE_FILE_OPEN_CSV:
+ label = i18n.getString("menuTableFileOpenCsv");
+ break;
+ case MID_TABLE_FILE_SAVE_CSV:
+ label = i18n.getString("menuTableFileSaveCsv");
+ break;
+ case MID_TABLE_FILE_SAVE_TEXT:
+ label = i18n.getString("menuTableFileSaveText");
+ break;
+
+ default:
+ throw new IllegalArgumentException("Invalid menu ID: " + id);
+ }
+
+ TMenuItem item = addItemInternal(id, label, key, enabled);
+ item.setCheckable(checkable);
+ return item;
+ }
+
+ /**
+ * Convenience function to add a menu separator.
+ */
+ public void addSeparator() {
+ int newY = getChildren().size() + 1;
+ assert (newY < getHeight());
+
+ // We just have to construct it, don't need to hang onto what it
+ // makes.
+ new TMenuSeparator(this, 1, newY);
+ setHeight(getHeight() + 1);
+ }
+
+ /**
+ * Convenience function to add a sub-menu.
+ *
+ * @param title menu title. Title must contain a keyboard shortcut,
+ * denoted by prefixing a letter with "&", e.g. "&File"
+ * @return the new sub-menu
+ */
+ public TSubMenu addSubMenu(final String title) {
+ int newY = getChildren().size() + 1;
+ assert (newY < getHeight());
+
+ TSubMenu subMenu = new TSubMenu(this, title, 1, newY);
+ setHeight(getHeight() + 1);
+ if (subMenu.getWidth() + 2 > getWidth()) {
+ setWidth(subMenu.getWidth() + 2);
+ }
+ for (TWidget widget: getChildren()) {
+ widget.setWidth(getWidth() - 2);
+ }
+ getApplication().recomputeMenuX();
+ activate(0);
+ subMenu.menu.setX(getX() + getWidth() - 2);
+
+ return subMenu;
+ }
+
+ }
--- /dev/null
+ menuNew=&New
+ menuExit=E&xit
+ menuShell=O&S Shell
+ menuOpen=&Open
+ menuCut=Cu&t
+ menuCopy=&Copy
+ menuPaste=&Paste
+ menuClear=C&lear
+ menuFind=&Find...
+ menuReplace=&Replace...
+ menuSearchAgain=&Search again
+ menuGotoLine=&Go to line number...
+ menuWindowTile=&Tile
+ menuWindowCascade=C&ascade
+ menuWindowCloseAll=Cl&ose All
+ menuWindowMove=&Size/Move
+ menuWindowZoom=&Zoom
+ menuWindowNext=&Next
+ menuWindowPrevious=&Previous
+ menuWindowClose=&Close
+ menuHelpContents=&Contents
+ menuHelpIndex=&Index
+ menuHelpSearch=&Topic search
+ menuHelpPrevious=&Previous topic
+ menuHelpHelp=&Help on help
+ menuHelpActive=Active &file...
+ menuHelpAbout=&About...
+
+ menuTableRenameRow=Rename &Row
+ menuTableRenameColumn=Rename C&olumn
+ menuTableViewRowLabels=&Row Labels
+ menuTableViewColumnLabels=&Column Labels
+ menuTableViewHighlightRow=Highlight Selected R&ow
+ menuTableViewHighlightColumn=Highlight Selected Co&lumn
+ menuTableBorderNone=N&one (Entire Table)
+ menuTableBorderAll=&All (Entire Table)
+ menuTableBorderCellNone=&None (Selected Cell)
+ menuTableBorderCellAll=All (&Selected Cell)
+ menuTableBorderRight=&Right
+ menuTableBorderLeft=&Left
+ menuTableBorderTop=&Top
+ menuTableBorderBottom=&Bottom
+ menuTableBorderDoubleBottom=Bottom (&Double)
+ menuTableBorderThickBottom=Bottom (T&hick)
+ menuTableDeleteLeft=Cell (Shift &Left)
+ menuTableDeleteUp=Cell (Shift &Up)
+ menuTableDeleteRow=Entire &Row
+ menuTableDeleteColumn=Entire &Column
+ menuTableInsertLeft=Column &Left
+ menuTableInsertRight=Column &Right
+ menuTableInsertAbove=Row &Above
+ menuTableInsertBelow=Row &Below
+ menuTableColumnNarrow=&Narrow
+ menuTableColumnWiden=&Widen
+ menuTableFileOpenCsv=Open &CSV...
+ menuTableFileSaveCsv=Save As C&SV...
+ menuTableFileSaveText=Save As &Text...
+
+ menuRepaintDesktop=&Repaint desktop
+ menuViewImage=&Open image...
+ menuScreenOptions=&Screen options...
+
--- /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.menu;
+
+ import jexer.TKeypress;
+ import jexer.TWidget;
+ import jexer.bits.CellAttributes;
+ import jexer.bits.GraphicsChars;
+ import jexer.bits.MnemonicString;
+ import jexer.bits.StringUtils;
+ import jexer.event.TKeypressEvent;
+ import jexer.event.TMouseEvent;
+ import jexer.event.TMenuEvent;
+ import static jexer.TKeypress.*;
+
+ /**
+ * TMenuItem implements a menu item.
+ */
+ public class TMenuItem extends TWidget {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Label for this menu item.
+ */
+ private String label;
+
+ /**
+ * Menu ID. IDs less than 1024 are reserved for common system
+ * functions. Existing ones are defined in TMenu, i.e. TMenu.MID_EXIT.
+ */
+ private int id = TMenu.MID_UNUSED;
+
+ /**
+ * When true, this item can be checked or unchecked.
+ */
+ private boolean checkable = false;
+
+ /**
+ * When true, this item is checked.
+ */
+ private boolean checked = false;
+
+ /**
+ * Global shortcut key.
+ */
+ private TKeypress key;
+
+ /**
+ * The title string. Use '&' to specify a mnemonic, i.e. "&File" will
+ * highlight the 'F' and allow 'f' or 'F' to select it.
+ */
+ private MnemonicString mnemonic;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * 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
+ */
+ TMenuItem(final TMenu parent, final int id, final int x, final int y,
+ final String label) {
+
+ // Set parent and window
+ super(parent);
+
+ mnemonic = new MnemonicString(label);
+
+ setX(x);
+ setY(y);
+ setHeight(1);
+ this.label = mnemonic.getRawLabel();
+ setWidth(StringUtils.width(label) + 4);
+ this.id = id;
+
+ // Default state for some known menu items
+ switch (id) {
+
+ case TMenu.MID_CUT:
+ setEnabled(false);
+ break;
+ case TMenu.MID_COPY:
+ setEnabled(false);
+ break;
+ case TMenu.MID_PASTE:
+ setEnabled(false);
+ break;
+ case TMenu.MID_CLEAR:
+ setEnabled(false);
+ break;
+
+ case TMenu.MID_TILE:
+ break;
+ case TMenu.MID_CASCADE:
+ break;
+ case TMenu.MID_CLOSE_ALL:
+ break;
+ case TMenu.MID_WINDOW_MOVE:
+ break;
+ case TMenu.MID_WINDOW_ZOOM:
+ break;
+ case TMenu.MID_WINDOW_NEXT:
+ break;
+ case TMenu.MID_WINDOW_PREVIOUS:
+ break;
+ case TMenu.MID_WINDOW_CLOSE:
+ break;
+ default:
+ break;
+ }
+
+ }
+
+ // ------------------------------------------------------------------------
+ // Event handlers ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Returns true if the mouse is currently on the menu item.
+ *
+ * @param mouse mouse event
+ * @return if true then the mouse is currently on this item
+ */
+ private boolean mouseOnMenuItem(final TMouseEvent mouse) {
+ if ((mouse.getY() == 0)
+ && (mouse.getX() >= 0)
+ && (mouse.getX() < getWidth())
+ ) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Handle mouse button releases.
+ *
+ * @param mouse mouse button release event
+ */
+ @Override
+ public void onMouseUp(final TMouseEvent mouse) {
+ if ((mouseOnMenuItem(mouse)) && (mouse.isMouse1())) {
+ dispatch();
+ return;
+ }
+ }
+
+ /**
+ * Handle keystrokes.
+ *
+ * @param keypress keystroke event
+ */
+ @Override
+ public void onKeypress(final TKeypressEvent keypress) {
+ if (keypress.equals(kbEnter)) {
+ dispatch();
+ return;
+ }
+
+ // Pass to parent for the things we don't care about.
+ super.onKeypress(keypress);
+ }
+
+ // ------------------------------------------------------------------------
+ // TWidget ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Draw a menu item with label.
+ */
+ @Override
+ public void draw() {
+ CellAttributes background = getTheme().getColor("tmenu");
+ CellAttributes menuColor;
+ CellAttributes menuMnemonicColor;
+ if (isAbsoluteActive()) {
+ menuColor = getTheme().getColor("tmenu.highlighted");
+ menuMnemonicColor = getTheme().getColor("tmenu.mnemonic.highlighted");
+ } else {
+ if (isEnabled()) {
+ menuColor = getTheme().getColor("tmenu");
+ menuMnemonicColor = getTheme().getColor("tmenu.mnemonic");
+ } else {
+ menuColor = getTheme().getColor("tmenu.disabled");
+ menuMnemonicColor = getTheme().getColor("tmenu.disabled");
+ }
+ }
+
+ 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);
+ 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);
+ }
+ if (checked) {
+ assert (checkable);
+ putCharXY(1, 0, GraphicsChars.CHECK, menuColor);
+ }
+
+ }
+
+ // ------------------------------------------------------------------------
+ // TMenuItem --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get the menu item ID.
+ *
+ * @return the id
+ */
+ public final int getId() {
+ return id;
+ }
+
+ /**
+ * Set checkable flag.
+ *
+ * @param checkable if true, this menu item can be checked/unchecked
+ */
+ public final void setCheckable(final boolean checkable) {
+ this.checkable = checkable;
+ }
+
+ /**
+ * Get checkable flag.
+ *
+ * @return true if this menu item is both checkable and checked
+ */
+ public final boolean getChecked() {
+ return ((checkable == true) && (checked == true));
+ }
+
+ /**
+ * Set checked flag. Note that setting checked on an item checkable will
+ * do nothing.
+ *
+ * @param checked if true, and if this menu item is checkable, then
+ * getChecked() will return true
+ */
+ public final void setChecked(final boolean checked) {
+ if (checkable) {
+ this.checked = checked;
+ } else {
+ this.checked = false;
+ }
+ }
+
+ /**
+ * Get the mnemonic string for this menu item.
+ *
+ * @return mnemonic string
+ */
+ public final MnemonicString getMnemonic() {
+ return mnemonic;
+ }
+
+ /**
+ * Get a global accelerator key for this menu item.
+ *
+ * @return global keyboard accelerator, or null if no key is associated
+ * with this item
+ */
+ public final TKeypress getKey() {
+ return key;
+ }
+
+ /**
+ * Set a global accelerator key for this menu item.
+ *
+ * @param key global keyboard accelerator
+ */
+ public final void setKey(final TKeypress key) {
+ this.key = key;
+
+ if (key != null) {
+ int newWidth = (StringUtils.width(label) + 4 +
+ StringUtils.width(key.toString()) + 2);
+ if (newWidth > getWidth()) {
+ setWidth(newWidth);
+ }
+ }
+ }
+
+ /**
+ * Dispatch event(s) due to selection or click.
+ */
+ public void dispatch() {
+ assert (isEnabled());
+
+ getApplication().postMenuEvent(new TMenuEvent(id));
+ if (checkable) {
+ checked = !checked;
+ }
+ }
+
+ }
--- /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.menu;
+
+ import jexer.bits.CellAttributes;
+ import jexer.bits.GraphicsChars;
+
+ /**
+ * TMenuSeparator is a special case menu item.
+ */
+ public class TMenuSeparator extends TMenuItem {
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Package private constructor.
+ *
+ * @param parent parent widget
+ * @param x column relative to parent
+ * @param y row relative to parent
+ */
+ TMenuSeparator(final TMenu parent, final int x, final int y) {
+ super(parent, TMenu.MID_UNUSED, x, y, "");
+ setEnabled(false);
+ setActive(false);
+ setWidth(parent.getWidth() - 2);
+ }
+
+ // ------------------------------------------------------------------------
+ // TMenuItem --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Draw a menu separator.
+ */
+ @Override
+ public void draw() {
+ CellAttributes background = getTheme().getColor("tmenu");
+
+ putCharXY(0, 0, GraphicsChars.CP437[0xC3], background);
+ putCharXY(getWidth() - 1, 0, GraphicsChars.CP437[0xB4], background);
+ hLineXY(1, 0, getWidth() - 2, GraphicsChars.SINGLE_BAR, background);
+ }
+
+ }
--- /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.menu;
+
+ import jexer.TKeypress;
+ import jexer.TWidget;
+ import jexer.bits.CellAttributes;
+ import jexer.bits.GraphicsChars;
+ import jexer.event.TKeypressEvent;
+ import static jexer.TKeypress.*;
+
+ /**
+ * TSubMenu is a special case menu item that wraps another TMenu.
+ */
+ public class TSubMenu extends TMenuItem {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The menu window. Note package private access.
+ */
+ TMenu menu;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Package private constructor.
+ *
+ * @param parent parent widget
+ * @param title menu title. Title must contain a keyboard shortcut,
+ * denoted by prefixing a letter with "&", e.g. "&File"
+ * @param x column relative to parent
+ * @param y row relative to parent
+ */
+ TSubMenu(final TMenu parent, final String title, final int x, final int y) {
+ super(parent, TMenu.MID_UNUSED, x, y, title);
+
+ setActive(false);
+ setEnabled(true);
+
+ this.menu = new TMenu(parent.getApplication(), x, getAbsoluteY() - 1,
+ title);
+ setWidth(menu.getWidth() + 2);
+
+ this.menu.isSubMenu = true;
+ }
+
+ // ------------------------------------------------------------------------
+ // Event handlers ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Handle keystrokes.
+ *
+ * @param keypress keystroke event
+ */
+ @Override
+ public void onKeypress(final TKeypressEvent keypress) {
+
+ // Open me if they hit my mnemonic.
+ if (!keypress.getKey().isFnKey()
+ && !keypress.getKey().isAlt()
+ && !keypress.getKey().isCtrl()
+ && (getMnemonic() != null)
+ && (Character.toLowerCase(getMnemonic().getShortcut())
+ == Character.toLowerCase(keypress.getKey().getChar()))
+ ) {
+ dispatch();
+ return;
+ }
+
+ if (menu.isActive()) {
+ menu.onKeypress(keypress);
+ return;
+ }
+
+ if (keypress.equals(kbEnter)) {
+ dispatch();
+ return;
+ }
+
+ if (keypress.equals(kbRight)) {
+ dispatch();
+ return;
+ }
+
+ if (keypress.equals(kbDown)) {
+ getParent().switchWidget(true);
+ return;
+ }
+
+ if (keypress.equals(kbUp)) {
+ getParent().switchWidget(false);
+ return;
+ }
+
+ if (keypress.equals(kbLeft)) {
+ TMenu parentMenu = (TMenu) getParent();
+ if (parentMenu.isSubMenu) {
+ getApplication().closeSubMenu();
+ } else {
+ getApplication().switchMenu(false);
+ }
+ return;
+ }
+
+ if (keypress.equals(kbEsc)) {
+ getApplication().closeMenu();
+ return;
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // TMenuItem --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Draw the menu title.
+ */
+ @Override
+ public void draw() {
+ super.draw();
+
+ CellAttributes menuColor;
+ if (isAbsoluteActive()) {
+ menuColor = getTheme().getColor("tmenu.highlighted");
+ } else {
+ if (isEnabled()) {
+ menuColor = getTheme().getColor("tmenu");
+ } else {
+ menuColor = getTheme().getColor("tmenu.disabled");
+ }
+ }
+
+ // Add the arrow
+ putCharXY(getWidth() - 2, 0, GraphicsChars.CP437[0x10], menuColor);
+ }
+
+ /**
+ * Override dispatch() to do nothing.
+ */
+ @Override
+ public void dispatch() {
+ assert (isEnabled());
+ if (isAbsoluteActive()) {
+ if (!menu.isActive()) {
+ getApplication().addSubMenu(menu);
+ menu.setActive(true);
+ }
+ }
+ }
+
+ /**
+ * Returns my active widget.
+ *
+ * @return widget that is active, or this if no children
+ */
+ @Override
+ public TWidget getActiveChild() {
+ if (menu.isActive()) {
+ return menu;
+ }
+ // Menu not active, return me
+ return this;
+ }
+
+ // ------------------------------------------------------------------------
+ // TSubMenu ---------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * 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
+ * @return the new menu item
+ */
+ public TMenuItem addItem(final int id, final String label,
+ final TKeypress key) {
+
+ return menu.addItem(id, label, key);
+ }
+
+ /**
+ * Convenience function to add a menu item.
+ *
+ * @param id menu item ID. Must be greater than 1024.
+ * @param label menu item label
+ * @return the new menu item
+ */
+ public TMenuItem addItem(final int id, final String label) {
+ return menu.addItem(id, label);
+ }
+
+ /**
+ * Convenience function to add one of the default menu items.
+ *
+ * @param id menu item ID. Must be between 0 (inclusive) and 1023
+ * (inclusive).
+ * @return the new menu item
+ */
+ public TMenuItem addDefaultItem(final int id) {
+ return menu.addDefaultItem(id);
+ }
+
+ /**
+ * Convenience function to add one of the default menu items.
+ *
+ * @param id menu item ID. Must be between 0 (inclusive) and 1023
+ * (inclusive).
+ * @param enabled default state for enabled
+ * @return the new menu item
+ */
+ public TMenuItem addDefaultItem(final int id, final boolean enabled) {
+ return menu.addDefaultItem(id, enabled);
+ }
+
+ /**
+ * Convenience function to add a menu separator.
+ */
+ public void addSeparator() {
+ menu.addSeparator();
+ }
+
+ /**
+ * Convenience function to add a sub-menu.
+ *
+ * @param title menu title. Title must contain a keyboard shortcut,
+ * denoted by prefixing a letter with "&", e.g. "&File"
+ * @return the new sub-menu
+ */
+ public TSubMenu addSubMenu(final String title) {
+ return menu.addSubMenu(title);
+ }
+
+ }
--- /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
+ */
+
+ /**
+ * Menu bar support classes.
+ */
+ package jexer.menu;
--- /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.net;
+
+ import java.io.InputStream;
+ import java.io.IOException;
+ import java.util.ArrayList;
+ import java.util.Map;
+ import java.util.TreeMap;
+
+ import jexer.backend.SessionInfo;
+ import static jexer.net.TelnetSocket.*;
+
+ /**
+ * TelnetInputStream works with TelnetSocket to perform the telnet protocol.
+ */
+ public class TelnetInputStream extends InputStream implements SessionInfo {
+
+ // ------------------------------------------------------------------------
+ // Constants --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The root TelnetSocket that has my telnet protocol state.
+ */
+ private TelnetSocket master;
+
+ /**
+ * The raw socket's InputStream.
+ */
+ private InputStream input;
+
+ /**
+ * The telnet-aware OutputStream.
+ */
+ private TelnetOutputStream output;
+
+ /**
+ * Persistent read buffer. In practice this will only be used if the
+ * single-byte read() is called sometime.
+ */
+ private byte [] readBuffer;
+
+ /**
+ * Current writing position in readBuffer - what is passed into
+ * input.read().
+ */
+ private int readBufferEnd;
+
+ /**
+ * Current read position in readBuffer - what is passed to the client in
+ * response to this.read().
+ */
+ private int readBufferStart;
+
+ /**
+ * User name.
+ */
+ private String username = "";
+
+ /**
+ * Language.
+ */
+ private String language = "en_US";
+
+ /**
+ * Text window width.
+ */
+ private int windowWidth = 80;
+
+ /**
+ * Text window height.
+ */
+ private int windowHeight = 24;
+
+ /**
+ * When true, the last read byte from the remote side was IAC.
+ */
+ private boolean iac = false;
+
+ /**
+ * When true, we are in the middle of a DO/DONT/WILL/WONT negotiation.
+ */
+ private boolean dowill = false;
+
+ /**
+ * The telnet option being negotiated.
+ */
+ private int dowillType = 0;
+
+ /**
+ * When true, we are waiting to see the end of the sub-negotiation
+ * sequence.
+ */
+ private boolean subnegEnd = false;
+
+ /**
+ * When true, the last byte read from the remote side was CR.
+ */
+ private boolean readCR = false;
+
+ /**
+ * The subnegotiation buffer.
+ */
+ private ArrayList<Byte> subnegBuffer;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Package private constructor.
+ *
+ * @param master the master TelnetSocket
+ * @param input the underlying socket's InputStream
+ * @param output the telnet-aware OutputStream
+ */
+ TelnetInputStream(final TelnetSocket master, final InputStream input,
+ final TelnetOutputStream output) {
+
+ this.master = master;
+ this.input = input;
+ this.output = output;
+
+ // Setup new read buffer
+ readBuffer = new byte[1024];
+ readBufferStart = 0;
+ readBufferEnd = 0;
+ subnegBuffer = new ArrayList<Byte>();
+ }
+
+ // ------------------------------------------------------------------------
+ // SessionInfo ------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Username getter.
+ *
+ * @return the username
+ */
+ public String getUsername() {
+ return this.username;
+ }
+
+ /**
+ * Username setter.
+ *
+ * @param username the value
+ */
+ public void setUsername(final String username) {
+ this.username = username;
+ }
+
+ /**
+ * Language getter.
+ *
+ * @return the language
+ */
+ public String getLanguage() {
+ return this.language;
+ }
+
+ /**
+ * Language setter.
+ *
+ * @param language the value
+ */
+ public void setLanguage(final String language) {
+ this.language = language;
+ }
+
+ /**
+ * Get the terminal type as reported by the telnet Terminal Type option.
+ *
+ * @return the terminal type
+ */
+ public String getTerminalType() {
+ return master.terminalType;
+ }
+
+ /**
+ * Text window width getter.
+ *
+ * @return the window width
+ */
+ public int getWindowWidth() {
+ return windowWidth;
+ }
+
+ /**
+ * Text window height getter.
+ *
+ * @return the window height
+ */
+ public int getWindowHeight() {
+ return windowHeight;
+ }
+
+ /**
+ * Re-query the text window size.
+ */
+ public void queryWindowSize() {
+ // NOP
+ }
+
+ // ------------------------------------------------------------------------
+ // InputStream ------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Returns an estimate of the number of bytes that can be read (or
+ * skipped over) from this input stream without blocking by the next
+ * invocation of a method for this input stream.
+ *
+ * @return an estimate of the number of bytes that can be read (or
+ * skipped over) from this input stream without blocking or 0 when it
+ * reaches the end of the input stream.
+ * @throws IOException if an I/O error occurs
+ */
+ @Override
+ public int available() throws IOException {
+ if (readBuffer == null) {
+ throw new IOException("InputStream is closed");
+ }
+ if (readBufferEnd - readBufferStart > 0) {
+ return (readBufferEnd - readBufferStart);
+ }
+ return input.available();
+ }
+
+ /**
+ * Closes this input stream and releases any system resources associated
+ * with the stream.
+ *
+ * @throws IOException if an I/O error occurs
+ */
+ @Override
+ public void close() throws IOException {
+ if (readBuffer != null) {
+ readBuffer = null;
+ input.close();
+ }
+ }
+
+ /**
+ * Marks the current position in this input stream.
+ *
+ * @param readLimit the maximum limit of bytes that can be read before
+ * the mark position becomes invalid
+ */
+ @Override
+ public void mark(final int readLimit) {
+ // Do nothing
+ }
+
+ /**
+ * Tests if this input stream supports the mark and reset methods.
+ *
+ * @return true if this stream instance supports the mark and reset
+ * methods; false otherwise
+ */
+ @Override
+ public boolean markSupported() {
+ return false;
+ }
+
+ /**
+ * Reads the next byte of data from the input stream.
+ *
+ * @return the next byte of data, or -1 if there is no more data because
+ * the end of the stream has been reached.
+ * @throws IOException if an I/O error occurs
+ */
+ @Override
+ public int read() throws IOException {
+
+ // If the post-processed buffer has bytes, use that.
+ if (readBufferEnd - readBufferStart > 0) {
+ readBufferStart++;
+ return readBuffer[readBufferStart - 1];
+ }
+
+ // The buffer is empty, so reset the indexes to 0.
+ readBufferStart = 0;
+ readBufferEnd = 0;
+
+ // Read some fresh data and run it through the telnet protocol.
+ int rc = readImpl(readBuffer, readBufferEnd,
+ readBuffer.length - readBufferEnd);
+
+ // If we got something, return it.
+ if (rc > 0) {
+ readBufferEnd += rc;
+ readBufferStart++;
+ return readBuffer[readBufferStart - 1];
+ }
+ // If we read 0, I screwed up big time.
+ assert (rc != 0);
+
+ // We read -1 (EOF).
+ return rc;
+ }
+
+ /**
+ * Reads some number of bytes from the input stream and stores them into
+ * the buffer array b.
+ *
+ * @param b the buffer into which the data is read.
+ * @return the total number of bytes read into the buffer, or -1 if there
+ * is no more data because the end of the stream has been reached.
+ * @throws IOException if an I/O error occurs
+ */
+ @Override
+ public int read(final byte[] b) throws IOException {
+ return read(b, 0, b.length);
+ }
+
+ /**
+ * Reads up to len bytes of data from the input stream into an array of
+ * bytes.
+ *
+ * @param b the buffer into which the data is read.
+ * @param off the start offset in array b at which the data is written.
+ * @param len the maximum number of bytes to read.
+ * @return the total number of bytes read into the buffer, or -1 if there
+ * is no more data because the end of the stream has been reached.
+ * @throws IOException if an I/O error occurs
+ */
+ @Override
+ public int read(final byte[] b, final int off,
+ final int len) throws IOException {
+
+ // The only time we can return 0 is if len is 0, as per the
+ // InputStream contract.
+ if (len == 0) {
+ return 0;
+ }
+
+ // If the post-processed buffer has bytes, use that.
+ if (readBufferEnd - readBufferStart > 0) {
+ int n = Math.min(len, readBufferEnd - readBufferStart);
+ System.arraycopy(b, off, readBuffer, readBufferStart, n);
+ readBufferStart += n;
+ return n;
+ }
+
+ // The buffer is empty, so reset the indexes to 0.
+ readBufferStart = 0;
+ readBufferEnd = 0;
+
+ // The maximum number of bytes we will ask for will definitely be
+ // within the bounds of what we can return in a single call.
+ int n = Math.min(len, readBuffer.length);
+
+ // Read some fresh data and run it through the telnet protocol.
+ int rc = readImpl(readBuffer, readBufferEnd, n);
+
+ // If we got something, return it.
+ if (rc > 0) {
+ System.arraycopy(readBuffer, 0, b, off, rc);
+ return rc;
+ }
+ // If we read 0, I screwed up big time.
+ assert (rc != 0);
+
+ // We read -1 (EOF).
+ return rc;
+ }
+
+ /**
+ * Repositions this stream to the position at the time the mark method
+ * was last called on this input stream. This is not supported by
+ * TelnetInputStream, so IOException is always thrown.
+ *
+ * @throws IOException if this function is used
+ */
+ @Override
+ public void reset() throws IOException {
+ throw new IOException("InputStream does not support mark/reset");
+ }
+
+ /**
+ * Skips over and discards n bytes of data from this input stream.
+ *
+ * @param n the number of bytes to be skipped
+ * @return the actual number of bytes skipped
+ * @throws IOException if an I/O error occurs
+ */
+ @Override
+ public long skip(final long n) throws IOException {
+ if (n < 0) {
+ return 0;
+ }
+ for (int i = 0; i < n; i++) {
+ read();
+ }
+ return n;
+ }
+
+ // ------------------------------------------------------------------------
+ // TelnetInputStream ------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * For debugging, return a descriptive string for this telnet option.
+ * These are pulled from: http://www.iana.org/assignments/telnet-options
+ *
+ * @param option the telnet option byte
+ * @return a string describing the telnet option code
+ */
+ @SuppressWarnings("unused")
+ private String optionString(final int option) {
+ switch (option) {
+ case 0: return "Binary Transmission";
+ case 1: return "Echo";
+ case 2: return "Reconnection";
+ case 3: return "Suppress Go Ahead";
+ case 4: return "Approx Message Size Negotiation";
+ case 5: return "Status";
+ case 6: return "Timing Mark";
+ case 7: return "Remote Controlled Trans and Echo";
+ case 8: return "Output Line Width";
+ case 9: return "Output Page Size";
+ case 10: return "Output Carriage-Return Disposition";
+ case 11: return "Output Horizontal Tab Stops";
+ case 12: return "Output Horizontal Tab Disposition";
+ case 13: return "Output Formfeed Disposition";
+ case 14: return "Output Vertical Tabstops";
+ case 15: return "Output Vertical Tab Disposition";
+ case 16: return "Output Linefeed Disposition";
+ case 17: return "Extended ASCII";
+ case 18: return "Logout";
+ case 19: return "Byte Macro";
+ case 20: return "Data Entry Terminal";
+ case 21: return "SUPDUP";
+ case 22: return "SUPDUP Output";
+ case 23: return "Send Location";
+ case 24: return "Terminal Type";
+ case 25: return "End of Record";
+ case 26: return "TACACS User Identification";
+ case 27: return "Output Marking";
+ case 28: return "Terminal Location Number";
+ case 29: return "Telnet 3270 Regime";
+ case 30: return "X.3 PAD";
+ case 31: return "Negotiate About Window Size";
+ case 32: return "Terminal Speed";
+ case 33: return "Remote Flow Control";
+ case 34: return "Linemode";
+ case 35: return "X Display Location";
+ case 36: return "Environment Option";
+ case 37: return "Authentication Option";
+ case 38: return "Encryption Option";
+ case 39: return "New Environment Option";
+ case 40: return "TN3270E";
+ case 41: return "XAUTH";
+ case 42: return "CHARSET";
+ case 43: return "Telnet Remote Serial Port (RSP)";
+ case 44: return "Com Port Control Option";
+ case 45: return "Telnet Suppress Local Echo";
+ case 46: return "Telnet Start TLS";
+ case 47: return "KERMIT";
+ case 48: return "SEND-URL";
+ case 49: return "FORWARD_X";
+ case 138: return "TELOPT PRAGMA LOGON";
+ case 139: return "TELOPT SSPI LOGON";
+ case 140: return "TELOPT PRAGMA HEARTBEAT";
+ case 255: return "Extended-Options-List";
+ default:
+ if ((option >= 50) && (option <= 137)) {
+ return "Unassigned";
+ }
+ return "UNKNOWN - OTHER";
+ }
+ }
+
+ /**
+ * Send a DO/DON'T/WILL/WON'T response to the remote side.
+ *
+ * @param response a TELNET_DO/DONT/WILL/WONT byte
+ * @param option telnet option byte (binary mode, term type, etc.)
+ * @throws IOException if an I/O error occurs
+ */
+ private void respond(final int response,
+ final int option) throws IOException {
+
+ byte [] buffer = new byte[3];
+ buffer[0] = (byte) TELNET_IAC;
+ buffer[1] = (byte) response;
+ buffer[2] = (byte) option;
+
+ output.rawWrite(buffer);
+ }
+
+ /**
+ * Tell the remote side we WILL support an option.
+ *
+ * @param option telnet option byte (binary mode, term type, etc.)
+ * @throws IOException if an I/O error occurs
+ */
+ private void WILL(final int option) throws IOException {
+ respond(TELNET_WILL, option);
+ }
+
+ /**
+ * Tell the remote side we WON'T support an option.
+ *
+ * @param option telnet option byte (binary mode, term type, etc.)
+ * @throws IOException if an I/O error occurs
+ */
+ private void WONT(final int option) throws IOException {
+ respond(TELNET_WONT, option);
+ }
+
+ /**
+ * Tell the remote side we DO support an option.
+ *
+ * @param option telnet option byte (binary mode, term type, etc.)
+ * @throws IOException if an I/O error occurs
+ */
+ private void DO(final int option) throws IOException {
+ respond(TELNET_DO, option);
+ }
+
+ /**
+ * Tell the remote side we DON'T support an option.
+ *
+ * @param option telnet option byte (binary mode, term type, etc.)
+ * @throws IOException if an I/O error occurs
+ */
+ private void DONT(final int option) throws IOException {
+ respond(TELNET_DONT, option);
+ }
+
+ /**
+ * Tell the remote side we WON't or DON'T support an option.
+ *
+ * @param remoteQuery a TELNET_DO/DONT/WILL/WONT byte
+ * @param option telnet option byte (binary mode, term type, etc.)
+ * @throws IOException if an I/O error occurs
+ */
+ private void refuse(final int remoteQuery,
+ final int option) throws IOException {
+
+ if (remoteQuery == TELNET_DO) {
+ WONT(option);
+ } else {
+ DONT(option);
+ }
+ }
+
+ /**
+ * Build sub-negotiation packet (RFC 855).
+ *
+ * @param option telnet option
+ * @param response output buffer of response bytes
+ * @throws IOException if an I/O error occurs
+ */
+ private void telnetSendSubnegResponse(final int option,
+ final byte [] response) throws IOException {
+
+ byte [] buffer = new byte[response.length + 5];
+ buffer[0] = (byte) TELNET_IAC;
+ buffer[1] = (byte) TELNET_SB;
+ buffer[2] = (byte) option;
+ System.arraycopy(response, 0, buffer, 3, response.length);
+ buffer[response.length + 3] = (byte) TELNET_IAC;
+ buffer[response.length + 4] = (byte) TELNET_SE;
+ output.rawWrite(buffer);
+ }
+
+ /**
+ * Telnet option: Terminal Speed (RFC 1079). Client side.
+ *
+ * @throws IOException if an I/O error occurs
+ */
+ private void telnetSendTerminalSpeed() throws IOException {
+ byte [] response = {0, '3', '8', '4', '0', '0', ',',
+ '3', '8', '4', '0', '0'};
+ telnetSendSubnegResponse(32, response);
+ }
+
+ /**
+ * Telnet option: Terminal Type (RFC 1091). Client side.
+ *
+ * @throws IOException if an I/O error occurs
+ */
+ private void telnetSendTerminalType() throws IOException {
+ byte [] response = {0, 'v', 't', '1', '0', '0' };
+ telnetSendSubnegResponse(24, response);
+ }
+
+ /**
+ * Telnet option: Terminal Type (RFC 1091). Server side.
+ *
+ * @throws IOException if an I/O error occurs
+ */
+ private void requestTerminalType() throws IOException {
+ byte [] response = new byte[1];
+ response[0] = 1;
+ telnetSendSubnegResponse(24, response);
+ }
+
+ /**
+ * Telnet option: Terminal Speed (RFC 1079). Server side.
+ *
+ * @throws IOException if an I/O error occurs
+ */
+ private void requestTerminalSpeed() throws IOException {
+ byte [] response = new byte[1];
+ response[0] = 1;
+ telnetSendSubnegResponse(32, response);
+ }
+
+ /**
+ * Telnet option: New Environment (RFC 1572). Server side.
+ *
+ * @throws IOException if an I/O error occurs
+ */
+ private void requestEnvironment() throws IOException {
+ byte [] response = new byte[1];
+ response[0] = 1;
+ telnetSendSubnegResponse(39, response);
+ }
+
+ /**
+ * Send the options we want to negotiate on.
+ *
+ * <p>The options we use are:
+ *
+ * <p>
+ * <pre>
+ * Binary Transmission RFC 856
+ * Suppress Go Ahead RFC 858
+ * Negotiate About Window Size RFC 1073
+ * Terminal Type RFC 1091
+ * Terminal Speed RFC 1079
+ * New Environment RFC 1572
+ *
+ * When run as a server:
+ * Echo RFC 857
+ * </pre>
+ *
+ * @throws IOException if an I/O error occurs
+ */
+ void telnetSendOptions() throws IOException {
+ if (master.binaryMode == false) {
+ // Binary Transmission: must ask both do and will
+ DO(0);
+ WILL(0);
+ }
+
+ if (master.goAhead == true) {
+ // Suppress Go Ahead
+ DO(3);
+ WILL(3);
+ }
+
+ // Server only options
+ if (master.isServer == true) {
+ // Enable Echo - I echo to them, they do not echo back to me.
+ DONT(1);
+ WILL(1);
+
+ if (master.doTermType == true) {
+ // Terminal type - request it
+ DO(24);
+ }
+
+ if (master.doTermSpeed == true) {
+ // Terminal speed - request it
+ DO(32);
+ }
+
+ if (master.doNAWS == true) {
+ // NAWS - request it
+ DO(31);
+ }
+
+ if (master.doEnvironment == true) {
+ // Environment - request it
+ DO(39);
+ }
+
+ } else {
+
+ if (master.doTermType == true) {
+ // Terminal type - request it
+ WILL(24);
+ }
+
+ if (master.doTermSpeed == true) {
+ // Terminal speed - request it
+ WILL(32);
+ }
+
+ if (master.doNAWS == true) {
+ // NAWS - request it
+ WILL(31);
+ }
+
+ if (master.doEnvironment == true) {
+ // Environment - request it
+ WILL(39);
+ }
+ }
+
+ // Push it all out
+ output.flush();
+ }
+
+ /**
+ * New Environment parsing state.
+ */
+ private enum EnvState {
+ INIT,
+ TYPE,
+ NAME,
+ VALUE
+ }
+
+ /**
+ * Handle the New Environment option. Note that this implementation
+ * fails to handle ESC as defined in RFC 1572.
+ */
+ private void handleNewEnvironment() {
+ Map<String, String> newEnv = new TreeMap<String, String>();
+
+ EnvState state = EnvState.INIT;
+ StringBuilder name = new StringBuilder();
+ StringBuilder value = new StringBuilder();
+
+ /*
+ System.err.printf("handleNewEnvironment() %d bytes\n",
+ subnegBuffer.size());
+ */
+
+ for (int i = 1; i < subnegBuffer.size(); i++) {
+ Byte b = subnegBuffer.get(i);
+ /*
+ System.err.printf(" b: %c %d 0x%02x\n", (char)b.byteValue(),
+ b, b);
+ */
+
+ switch (state) {
+
+ case INIT:
+ // Looking for "IS"
+ if (b == 0) {
+ state = EnvState.TYPE;
+ } else {
+ // The other side isn't following the rules, see ya.
+ return;
+ }
+ break;
+
+ case TYPE:
+ // Looking for "VAR" or "USERVAR"
+ if (b == 0) {
+ // VAR
+ state = EnvState.NAME;
+ name = new StringBuilder();
+ } else if (b == 3) {
+ // USERVAR
+ state = EnvState.NAME;
+ name = new StringBuilder();
+ } else {
+ // The other side isn't following the rules, see ya
+ return;
+ }
+ break;
+
+ case NAME:
+ // Looking for "VALUE" or a name byte
+ if (b == 1) {
+ // VALUE
+ state = EnvState.VALUE;
+ value = new StringBuilder();
+ } else {
+ // Take it as an environment variable name/key byte
+ name.append((char)b.byteValue());
+ }
+
+ break;
+
+ case VALUE:
+ // Looking for "VAR", "USERVAR", or a name byte, or the end
+ if (b == 0) {
+ // VAR
+ state = EnvState.NAME;
+ if (value.length() > 0) {
+ /*
+ System.err.printf("NAME: '%s' VALUE: '%s'\n",
+ name, value);
+ */
+ newEnv.put(name.toString(), value.toString());
+ }
+ name = new StringBuilder();
+ } else if (b == 3) {
+ // USERVAR
+ state = EnvState.NAME;
+ if (value.length() > 0) {
+ /*
+ System.err.printf("NAME: '%s' VALUE: '%s'\n",
+ name, value);
+ */
+ newEnv.put(name.toString(), value.toString());
+ }
+ name = new StringBuilder();
+ } else {
+ // Take it as an environment variable value byte
+ value.append((char)b.byteValue());
+ }
+ break;
+
+ default:
+ throw new RuntimeException("Invalid state: " + state);
+
+ }
+ }
+
+ if ((name.length() > 0) && (value.length() > 0)) {
+ /*
+ System.err.printf("NAME: '%s' VALUE: '%s'\n", name, value);
+ */
+ newEnv.put(name.toString(), value.toString());
+ }
+
+ for (String key: newEnv.keySet()) {
+ if (key.equals("LANG")) {
+ language = newEnv.get(key);
+ }
+ if (key.equals("LOGNAME")) {
+ username = newEnv.get(key);
+ }
+ if (key.equals("USER")) {
+ username = newEnv.get(key);
+ }
+ }
+ }
+
+ /**
+ * Handle an option sub-negotiation.
+ *
+ * @throws IOException if an I/O error occurs
+ */
+ private void handleSubneg() throws IOException {
+ Byte option;
+
+ // Sanity check: there must be at least 1 byte in subnegBuffer
+ if (subnegBuffer.size() < 1) {
+ // Buffer too small: the other side is a broken telnetd, it did
+ // not send the right sub-negotiation data. Bail out now.
+ return;
+ }
+ option = subnegBuffer.get(0);
+
+ switch (option) {
+
+ case 24:
+ // Terminal Type
+ if ((subnegBuffer.size() > 1) && (subnegBuffer.get(1) == 1)) {
+ // Server sent "SEND", we say "IS"
+ telnetSendTerminalType();
+ }
+ if ((subnegBuffer.size() > 1) && (subnegBuffer.get(1) == 0)) {
+ // Client sent "IS", record it
+ StringBuilder terminalString = new StringBuilder();
+ for (int i = 2; i < subnegBuffer.size(); i++) {
+ terminalString.append((char)subnegBuffer.
+ get(i).byteValue());
+ }
+ master.terminalType = terminalString.toString();
+ /*
+ System.err.printf("terminal type: '%s'\n",
+ master.terminalType);
+ */
+ }
+ break;
+
+ case 32:
+ // Terminal Speed
+ if ((subnegBuffer.size() > 1) && (subnegBuffer.get(1) == 1)) {
+ // Server sent "SEND", we say "IS"
+ telnetSendTerminalSpeed();
+ }
+ if ((subnegBuffer.size() > 1) && (subnegBuffer.get(1) == 0)) {
+ // Client sent "IS", record it
+ StringBuilder speedString = new StringBuilder();
+ for (int i = 2; i < subnegBuffer.size(); i++) {
+ speedString.append((char)subnegBuffer.get(i).byteValue());
+ }
+ master.terminalSpeed = speedString.toString();
+ /*
+ System.err.printf("terminal speed: '%s'\n",
+ master.terminalSpeed);
+ */
+ }
+ break;
+
+ case 31:
+ // NAWS
+ if (subnegBuffer.size() >= 5) {
+ int i = 0;
+
+ i++;
+ if (subnegBuffer.get(i) == (byte) TELNET_IAC) {
+ i++;
+ }
+ int width = subnegBuffer.get(i);
+ if (width < 0) {
+ width += 256;
+ }
+ windowWidth = width * 256;
+
+ i++;
+ if (subnegBuffer.get(i) == (byte) TELNET_IAC) {
+ i++;
+ }
+ width = subnegBuffer.get(i);
+ windowWidth += width;
+ if (width < 0) {
+ windowWidth += 256;
+ }
+
+ i++;
+ if (subnegBuffer.get(i) == (byte) TELNET_IAC) {
+ i++;
+ }
+ int height = subnegBuffer.get(i);
+ if (height < 0) {
+ height += 256;
+ }
+ windowHeight = height * 256;
+
+ i++;
+ if (subnegBuffer.get(i) == (byte) TELNET_IAC) {
+ i++;
+ }
+ height = subnegBuffer.get(i);
+ windowHeight += height;
+ if (height < 0) {
+ windowHeight += 256;
+ }
+ }
+ break;
+
+ case 39:
+ // Environment
+ handleNewEnvironment();
+ break;
+
+ default:
+ // Ignore this one
+ break;
+ }
+ }
+
+ /**
+ * Reads up to len bytes of data from the input stream into an array of
+ * bytes.
+ *
+ * @param buf the buffer into which the data is read.
+ * @param off the start offset in array b at which the data is written.
+ * @param len the maximum number of bytes to read.
+ * @return the total number of bytes read into the buffer, or -1 if there
+ * is no more data because the end of the stream has been reached.
+ * @throws IOException if an I/O error occurs
+ */
+ private int readImpl(final byte[] buf, final int off,
+ final int len) throws IOException {
+
+ assert (len > 0);
+
+ // The current writing position in buf.
+ int bufN = off;
+
+ // We will keep trying to read() until we have something to return.
+ do {
+
+ byte [] buffer = null;
+ if (master.binaryMode) {
+ // Binary mode: read up to len bytes. There will never be
+ // more bytes to pass upstream than there are bytes on the
+ // wire.
+ buffer = new byte[len];
+ } else {
+ // ASCII mode: read up to len - 2 bytes. There may have been
+ // some combination of IAC, CR, and NUL from a previous
+ // readImpl() that could result in more bytes to pass up than
+ // are on the wire.
+ buffer = new byte[len - 2];
+ }
+
+ int bufferN = 0;
+
+ // Read some data from the other end
+ int rc = input.read(buffer);
+
+ // Check for EOF or error
+ if (rc > 0) {
+ // More data came in
+ bufferN = rc;
+ } else {
+ // EOF, just return it.
+ return rc;
+ }
+
+ // Loop through the read bytes
+ for (int i = 0; i < bufferN; i++) {
+ byte b = buffer[i];
+
+ if (subnegEnd == true) {
+ // Looking for IAC SE to end this subnegotiation
+ if (b == (byte) TELNET_SE) {
+ if (iac == true) {
+ iac = false;
+ subnegEnd = false;
+ handleSubneg();
+ }
+ } else if (b == (byte) TELNET_IAC) {
+ if (iac == true) {
+ // An argument to the subnegotiation option
+ subnegBuffer.add((byte) TELNET_IAC);
+ } else {
+ iac = true;
+ }
+ } else {
+ // An argument to the subnegotiation option
+ subnegBuffer.add(b);
+ }
+ continue;
+ }
+
+ // Look for DO/DON'T/WILL/WON'T option
+ if (dowill == true) {
+
+ // Look for option/
+ switch (b) {
+
+ case 0:
+ // Binary Transmission
+ if (dowillType == (byte) TELNET_WILL) {
+ // Server will use binary transmission, yay.
+ master.binaryMode = true;
+ } else if (dowillType == (byte) TELNET_DO) {
+ // Server asks for binary transmission.
+ WILL(b);
+ master.binaryMode = true;
+ } else if (dowillType == (byte) TELNET_WONT) {
+ // We're screwed, server won't do binary
+ // transmission.
+ master.binaryMode = false;
+ } else {
+ // Server demands NVT ASCII mode.
+ master.binaryMode = false;
+ }
+ break;
+
+ case 1:
+ // Echo
+ if (dowillType == (byte) TELNET_WILL) {
+ // Server will use echo, yay.
+ master.echoMode = true;
+ } else if (dowillType == (byte) TELNET_DO) {
+ // Server asks for echo.
+ WILL(b);
+ master.echoMode = true;
+ } else if (dowillType == (byte) TELNET_WONT) {
+ // We're screwed, server won't do echo.
+ master.echoMode = false;
+ } else {
+ // Server demands no echo.
+ master.echoMode = false;
+ }
+ break;
+
+ case 3:
+ // Suppress Go Ahead
+ if (dowillType == (byte) TELNET_WILL) {
+ // Server will use suppress go-ahead, yay.
+ master.goAhead = false;
+ } else if (dowillType == (byte) TELNET_DO) {
+ // Server asks for suppress go-ahead.
+ WILL(b);
+ master.goAhead = false;
+ } else if (dowillType == (byte) TELNET_WONT) {
+ // We're screwed, server won't do suppress
+ // go-ahead.
+ master.goAhead = true;
+ } else {
+ // Server demands Go-Ahead mode.
+ master.goAhead = true;
+ }
+ break;
+
+ case 24:
+ // Terminal Type - send what's in TERM
+ if (dowillType == (byte) TELNET_WILL) {
+ // Server will use terminal type, yay.
+ if (master.isServer
+ && master.doTermType
+ ) {
+ requestTerminalType();
+ master.doTermType = false;
+ } else if (!master.isServer) {
+ master.doTermType = true;
+ }
+ } else if (dowillType == (byte) TELNET_DO) {
+ // Server asks for terminal type.
+ WILL(b);
+ master.doTermType = true;
+ } else if (dowillType == (byte) TELNET_WONT) {
+ // We're screwed, server won't do terminal type.
+ master.doTermType = false;
+ } else {
+ // Server will not listen to terminal type.
+ master.doTermType = false;
+ }
+ break;
+
+ case 31:
+ // NAWS
+ if (dowillType == (byte) TELNET_WILL) {
+ // Server will use NAWS, yay.
+ master.doNAWS = true;
+ // NAWS cannot be requested by the server, it is
+ // only sent by the client.
+ } else if (dowillType == (byte) TELNET_DO) {
+ // Server asks for NAWS.
+ WILL(b);
+ master.doNAWS = true;
+ } else if (dowillType == (byte) TELNET_WONT) {
+ // Server won't do NAWS.
+ master.doNAWS = false;
+ } else {
+ // Server will not listen to NAWS.
+ master.doNAWS = false;
+ }
+ break;
+
+ case 32:
+ // Terminal Speed
+ if (dowillType == (byte) TELNET_WILL) {
+ // Server will use terminal speed, yay.
+ if (master.isServer
+ && master.doTermSpeed
+ ) {
+ requestTerminalSpeed();
+ master.doTermSpeed = false;
+ } else if (!master.isServer) {
+ master.doTermSpeed = true;
+ }
+ } else if (dowillType == (byte) TELNET_DO) {
+ // Server asks for terminal speed.
+ WILL(b);
+ master.doTermSpeed = true;
+ } else if (dowillType == (byte) TELNET_WONT) {
+ // We're screwed, server won't do terminal speed.
+ master.doTermSpeed = false;
+ } else {
+ // Server will not listen to terminal speed.
+ master.doTermSpeed = false;
+ }
+ break;
+
+ case 39:
+ // New Environment
+ if (dowillType == (byte) TELNET_WILL) {
+ // Server will use NewEnvironment, yay.
+ if (master.isServer
+ && master.doEnvironment
+ ) {
+ requestEnvironment();
+ master.doEnvironment = false;
+ } else if (!master.isServer) {
+ master.doEnvironment = true;
+ }
+ } else if (dowillType == (byte) TELNET_DO) {
+ // Server asks for NewEnvironment.
+ WILL(b);
+ master.doEnvironment = true;
+ } else if (dowillType == (byte) TELNET_WONT) {
+ // Server won't do NewEnvironment.
+ master.doEnvironment = false;
+ } else {
+ // Server will not listen to New Environment.
+ master.doEnvironment = false;
+ }
+ break;
+
+
+ default:
+ // Other side asked for something we don't
+ // understand. Tell them we will not do this option.
+ refuse(dowillType, b);
+ break;
+ }
+
+ dowill = false;
+ continue;
+ } // if (dowill == true)
+
+ // Perform read processing
+ if (b == (byte) TELNET_IAC) {
+
+ // Telnet command
+ if (iac == true) {
+ // IAC IAC -> IAC
+ buf[bufN++] = (byte) TELNET_IAC;
+ iac = false;
+ } else {
+ iac = true;
+ }
+ continue;
+ } else {
+ if (iac == true) {
+
+ switch (b) {
+
+ case (byte) TELNET_SE:
+ // END Sub-Negotiation
+ break;
+ case (byte) TELNET_NOP:
+ // NOP
+ break;
+ case (byte) TELNET_DM:
+ // Data Mark
+ break;
+ case (byte) TELNET_BRK:
+ // Break
+ break;
+ case (byte) TELNET_IP:
+ // Interrupt Process
+ break;
+ case (byte) TELNET_AO:
+ // Abort Output
+ break;
+ case (byte) TELNET_AYT:
+ // Are You There?
+ break;
+ case (byte) TELNET_EC:
+ // Erase Character
+ break;
+ case (byte) TELNET_EL:
+ // Erase Line
+ break;
+ case (byte) TELNET_GA:
+ // Go Ahead
+ break;
+ case (byte) TELNET_SB:
+ // START Sub-Negotiation
+ // From here we wait for the IAC SE
+ subnegEnd = true;
+ subnegBuffer.clear();
+ break;
+ case (byte) TELNET_WILL:
+ // WILL
+ dowill = true;
+ dowillType = b;
+ break;
+ case (byte) TELNET_WONT:
+ // WON'T
+ dowill = true;
+ dowillType = b;
+ break;
+ case (byte) TELNET_DO:
+ // DO
+ dowill = true;
+ dowillType = b;
+ break;
+ case (byte) TELNET_DONT:
+ // DON'T
+ dowill = true;
+ dowillType = b;
+ break;
+ default:
+ // This should be equivalent to IAC NOP
+ break;
+ }
+ iac = false;
+ continue;
+
+ } // if (iac == true)
+
+ /*
+ * All of the regular IAC processing is completed at this
+ * point. Now we need to handle the CR and CR LF cases.
+ *
+ * According to RFC 854, in NVT ASCII mode:
+ * Bare CR -> CR NUL
+ * CR LF -> CR LF
+ *
+ */
+ if (master.binaryMode == false) {
+
+ if (b == C_LF) {
+ if (readCR == true) {
+ // This is CR LF. Send CR LF and turn the cr
+ // flag off.
+ buf[bufN++] = C_CR;
+ buf[bufN++] = C_LF;
+ readCR = false;
+ continue;
+ }
+ // This is bare LF. Send LF.
+ buf[bufN++] = C_LF;
+ continue;
+ }
+
+ if (b == C_NUL) {
+ if (readCR == true) {
+ // This is CR NUL. Send CR and turn the cr
+ // flag off.
+ buf[bufN++] = C_CR;
+ readCR = false;
+ continue;
+ }
+ // This is bare NUL. Send NUL.
+ buf[bufN++] = C_NUL;
+ continue;
+ }
+
+ if (b == C_CR) {
+ if (readCR == true) {
+ // This is CR CR. Send a CR NUL and leave
+ // the cr flag on.
+ buf[bufN++] = C_CR;
+ buf[bufN++] = C_NUL;
+ continue;
+ }
+ // This is the first CR. Set the cr flag.
+ readCR = true;
+ continue;
+ }
+
+ if (readCR == true) {
+ // This was a bare CR in the stream.
+ buf[bufN++] = C_CR;
+ readCR = false;
+ }
+
+ // This is a regular character. Pass it on.
+ buf[bufN++] = b;
+ continue;
+ }
+
+ /*
+ * This is the case for any of:
+ *
+ * 1) A NVT ASCII character that isn't CR, LF, or
+ * NUL.
+ *
+ * 2) A NVT binary character.
+ *
+ * For all of these cases, we just pass the character on.
+ */
+ buf[bufN++] = b;
+
+ } // if (b == TELNET_IAC)
+
+ } // for (int i = 0; i < bufferN; i++)
+
+ } while (bufN == 0);
+
+ // Return bytes read
+ return bufN;
+ }
+
+ }
--- /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.net;
+
+ import java.io.OutputStream;
+ import java.io.IOException;
+
+ import static jexer.net.TelnetSocket.*;
+
+ /**
+ * TelnetOutputStream works with TelnetSocket to perform the telnet protocol.
+ */
+ public class TelnetOutputStream extends OutputStream {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The root TelnetSocket that has my telnet protocol state.
+ */
+ private TelnetSocket master;
+
+ /**
+ * The raw socket's OutputStream.
+ */
+ private OutputStream output;
+
+ /**
+ * When true, the last byte the caller passed to write() was a CR.
+ */
+ private boolean writeCR = false;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Package private constructor.
+ *
+ * @param master the master TelnetSocket
+ * @param output the underlying socket's OutputStream
+ */
+ TelnetOutputStream(final TelnetSocket master, final OutputStream output) {
+ this.master = master;
+ this.output = output;
+ }
+
+ // ------------------------------------------------------------------------
+ // OutputStrem ------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Closes this output stream and releases any system resources associated
+ * with this stream.
+ *
+ * @throws IOException if an I/O error occurs
+ */
+ @Override
+ public void close() throws IOException {
+ if (output != null) {
+ output.close();
+ output = null;
+ }
+ }
+
+ /**
+ * Flushes this output stream and forces any buffered output bytes to be
+ * written out.
+ *
+ * @throws IOException if an I/O error occurs
+ */
+ @Override
+ public void flush() throws IOException {
+ if ((master.binaryMode == false) && (writeCR == true)) {
+ // The last byte sent to this.write() was a CR, which was never
+ // actually sent. So send the CR in ascii mode, then flush.
+ // CR <anything> -> CR NULL
+ output.write(C_CR);
+ output.write(C_NUL);
+ writeCR = false;
+ }
+ output.flush();
+ }
+
+ /**
+ * Writes b.length bytes from the specified byte array to this output
+ * stream.
+ *
+ * @param b the data.
+ * @throws IOException if an I/O error occurs
+ */
+ @Override
+ public void write(final byte[] b) throws IOException {
+ writeImpl(b, 0, b.length);
+ }
+
+ /**
+ * Writes len bytes from the specified byte array starting at offset off
+ * to this output stream.
+ *
+ * @param b the data.
+ * @param off the start offset in the data.
+ * @param len the number of bytes to write.
+ * @throws IOException if an I/O error occurs
+ */
+ @Override
+ public void write(final byte[] b, final int off,
+ final int len) throws IOException {
+
+ writeImpl(b, off, len);
+ }
+
+ /**
+ * Writes the specified byte to this output stream.
+ *
+ * @param b the byte to write.
+ * @throws IOException if an I/O error occurs
+ */
+ @Override
+ public void write(final int b) throws IOException {
+ byte [] bytes = new byte[1];
+ bytes[0] = (byte) b;
+ writeImpl(bytes, 0, 1);
+ }
+
+ // ------------------------------------------------------------------------
+ // TelnetOutputStrem ------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Writes b.length bytes from the specified byte array to this output
+ * stream. Note package private access.
+ *
+ * @param b the data.
+ * @throws IOException if an I/O error occurs
+ */
+ void rawWrite(final byte[] b) throws IOException {
+ output.write(b, 0, b.length);
+ }
+
+ /**
+ * Writes len bytes from the specified byte array starting at offset off
+ * to this output stream.
+ *
+ * @param b the data.
+ * @param off the start offset in the data.
+ * @param len the number of bytes to write.
+ * @throws IOException if an I/O error occurs
+ */
+ private void writeImpl(final byte[] b, final int off,
+ final int len) throws IOException {
+
+ byte [] writeBuffer = new byte[Math.max(len, 4)];
+ int writeBufferI = 0;
+
+ for (int i = 0; i < len; i++) {
+ if (writeBufferI >= writeBuffer.length - 4) {
+ // Flush what we have generated so far and reset the buffer,
+ // because the next byte could generate up to 4 output bytes
+ // (CR <something> <IAC> <IAC>).
+ output.write(writeBuffer, 0, writeBufferI);
+ writeBufferI = 0;
+ }
+
+ // Pull the next byte
+ byte ch = b[i + off];
+
+ if (master.binaryMode == true) {
+
+ if (ch == TELNET_IAC) {
+ // IAC -> IAC IAC
+ writeBuffer[writeBufferI++] = (byte) TELNET_IAC;
+ writeBuffer[writeBufferI++] = (byte) TELNET_IAC;
+ } else {
+ // Anything else -> just send
+ writeBuffer[writeBufferI++] = ch;
+ }
+ continue;
+ }
+
+ // Non-binary mode: more complicated. We use writeCR to handle
+ // the case that the last byte of b was a CR.
+
+ // Bare carriage return -> CR NUL
+ if (ch == C_CR) {
+ if (writeCR == true) {
+ // Flush the previous CR to the stream.
+ // CR <anything> -> CR NULL
+ writeBuffer[writeBufferI++] = (byte) C_CR;
+ writeBuffer[writeBufferI++] = (byte) C_NUL;
+ }
+ writeCR = true;
+ } else if (ch == C_LF) {
+ if (writeCR == true) {
+ // CR LF -> CR LF
+ writeBuffer[writeBufferI++] = (byte) C_CR;
+ writeBuffer[writeBufferI++] = (byte) C_LF;
+ writeCR = false;
+ } else {
+ // Bare LF -> LF
+ writeBuffer[writeBufferI++] = ch;
+ }
+ } else if (ch == TELNET_IAC) {
+ if (writeCR == true) {
+ // CR <anything> -> CR NULL
+ writeBuffer[writeBufferI++] = (byte) C_CR;
+ writeBuffer[writeBufferI++] = (byte) C_NUL;
+ writeCR = false;
+ }
+ // IAC -> IAC IAC
+ writeBuffer[writeBufferI++] = (byte) TELNET_IAC;
+ writeBuffer[writeBufferI++] = (byte) TELNET_IAC;
+ } else {
+ if (writeCR == true) {
+ // CR <anything> -> CR NULL
+ writeBuffer[writeBufferI++] = (byte) C_CR;
+ writeBuffer[writeBufferI++] = (byte) C_NUL;
+ writeCR = false;
+ } else {
+ // Normal character */
+ writeBuffer[writeBufferI++] = ch;
+ }
+ }
+
+ } // while (i < userbuf.length)
+
+ if (writeBufferI > 0) {
+ // Flush what we have generated so far and reset the buffer.
+ output.write(writeBuffer, 0, writeBufferI);
+ }
+ }
+
+ }
--- /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.net;
+
+ import java.io.IOException;
+ import java.net.InetAddress;
+ import java.net.ServerSocket;
+ import java.net.Socket;
+ import java.net.SocketException;
+
+ /**
+ * This class provides a ServerSocket that return TelnetSocket's in accept().
+ */
+ public class TelnetServerSocket extends ServerSocket {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Creates an unbound server socket.
+ *
+ * @throws IOException if an I/O error occurs
+ */
+ public TelnetServerSocket() throws IOException {
+ super();
+ }
+
+ /**
+ * Creates a server socket, bound to the specified port.
+ *
+ * @param port the port number, or 0 to use a port number that is
+ * automatically allocated.
+ * @throws IOException if an I/O error occurs
+ */
+ public TelnetServerSocket(final int port) throws IOException {
+ super(port);
+ }
+
+ /**
+ * Creates a server socket and binds it to the specified local port
+ * number, with the specified backlog.
+ *
+ * @param port the port number, or 0 to use a port number that is
+ * automatically allocated.
+ * @param backlog requested maximum length of the queue of incoming
+ * connections.
+ * @throws IOException if an I/O error occurs
+ */
+ public TelnetServerSocket(final int port,
+ final int backlog) throws IOException {
+
+ super(port, backlog);
+ }
+
+ /**
+ * Create a server with the specified port, listen backlog, and local IP
+ * address to bind to.
+ *
+ * @param port the port number, or 0 to use a port number that is
+ * automatically allocated.
+ * @param backlog requested maximum length of the queue of incoming
+ * connections.
+ * @param bindAddr the local InetAddress the server will bind to
+ * @throws IOException if an I/O error occurs
+ */
+ public TelnetServerSocket(final int port, final int backlog,
+ final InetAddress bindAddr) throws IOException {
+
+ super(port, backlog, bindAddr);
+ }
+
+ // ------------------------------------------------------------------------
+ // ServerSocket -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Listens for a connection to be made to this socket and accepts it. The
+ * method blocks until a connection is made.
+ *
+ * @return the new Socket
+ * @throws IOException if an I/O error occurs
+ */
+ @Override
+ public Socket accept() throws IOException {
+ if (isClosed()) {
+ throw new SocketException("Socket is closed");
+ }
+ if (!isBound()) {
+ throw new SocketException("Socket is not bound");
+ }
+
+ Socket socket = new TelnetSocket();
+ implAccept(socket);
+ return socket;
+ }
+
+ }
--- /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.net;
+
+ import java.io.InputStream;
+ import java.io.IOException;
+ import java.io.OutputStream;
+ import java.net.Socket;
+
+ /**
+ * This class provides a Socket that performs the telnet protocol to both
+ * establish an 8-bit clean no echo channel and expose window resize events
+ * to the Jexer ECMA48 backend.
+ */
+ public class TelnetSocket extends Socket {
+
+ // ------------------------------------------------------------------------
+ // Constants --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ // Telnet protocol special characters. Note package private access.
+ static final int TELNET_SE = 240;
+ static final int TELNET_NOP = 241;
+ static final int TELNET_DM = 242;
+ static final int TELNET_BRK = 243;
+ static final int TELNET_IP = 244;
+ static final int TELNET_AO = 245;
+ static final int TELNET_AYT = 246;
+ static final int TELNET_EC = 247;
+ static final int TELNET_EL = 248;
+ static final int TELNET_GA = 249;
+ static final int TELNET_SB = 250;
+ static final int TELNET_WILL = 251;
+ static final int TELNET_WONT = 252;
+ static final int TELNET_DO = 253;
+ static final int TELNET_DONT = 254;
+ static final int TELNET_IAC = 255;
+ static final int C_NUL = 0x00;
+ static final int C_LF = 0x0A;
+ static final int C_CR = 0x0D;
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The telnet-aware socket InputStream.
+ */
+ private TelnetInputStream input;
+
+ /**
+ * The telnet-aware socket OutputStream.
+ */
+ private TelnetOutputStream output;
+
+
+ /**
+ * If true, this is a server socket (i.e. created by accept()).
+ */
+ boolean isServer = true;
+
+ /**
+ * If true, telnet ECHO mode is set such that local echo is off and
+ * remote echo is on. This is appropriate for server sockets.
+ */
+ boolean echoMode = false;
+
+ /**
+ * If true, telnet BINARY mode is enabled. We always want this to
+ * ensure a Unicode-safe stream.
+ */
+ boolean binaryMode = false;
+
+ /**
+ * If true, the SUPPRESS-GO-AHEAD option is enabled. We always want
+ * this.
+ */
+ boolean goAhead = true;
+
+ /**
+ * If true, request the client terminal type.
+ */
+ boolean doTermType = true;
+
+ /**
+ * If true, request the client terminal speed.
+ */
+ boolean doTermSpeed = true;
+
+ /**
+ * If true, request the Negotiate About Window Size option to
+ * determine the client text width/height.
+ */
+ boolean doNAWS = true;
+
+ /**
+ * If true, request the New Environment option to obtain the client
+ * LOGNAME, USER, and LANG variables.
+ */
+ boolean doEnvironment = true;
+
+ /**
+ * The terminal type reported by the client.
+ */
+ String terminalType = "";
+
+ /**
+ * The terminal speed reported by the client.
+ */
+ String terminalSpeed = "";
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Creates a Socket that knows the telnet protocol. Note package private
+ * access, this is only used by TelnetServerSocket.
+ *
+ * @throws IOException if an I/O error occurs
+ */
+ TelnetSocket() throws IOException {
+ super();
+ }
+
+ // ------------------------------------------------------------------------
+ // Socket -----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Returns an input stream for this socket.
+ *
+ * @return the input stream
+ * @throws IOException if an I/O error occurs
+ */
+ @Override
+ public InputStream getInputStream() throws IOException {
+ if (input == null) {
+ assert (output == null);
+ output = new TelnetOutputStream(this, super.getOutputStream());
+ input = new TelnetInputStream(this, super.getInputStream(), output);
+ input.telnetSendOptions();
+ }
+ return input;
+ }
+
+ /**
+ * Returns an output stream for this socket.
+ *
+ * @return the output stream
+ * @throws IOException if an I/O error occurs
+ */
+ @Override
+ public OutputStream getOutputStream() throws IOException {
+ if (output == null) {
+ assert (input == null);
+ output = new TelnetOutputStream(this, super.getOutputStream());
+ input = new TelnetInputStream(this, super.getInputStream(), output);
+ input.telnetSendOptions();
+ }
+ return output;
+ }
+
+ // ------------------------------------------------------------------------
+ // TelnetSocket -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * See if telnet server/client is in ASCII mode.
+ *
+ * @return if true, this connection is in ASCII mode
+ */
+ public boolean isAscii() {
+ return (!binaryMode);
+ }
+
+ }
--- /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
+ */
+
+ /**
+ * A Telnet-aware ServerSocket that establishes an 8-bit clean data channel.
+ */
+ package jexer.net;
--- /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
+ */
+
+ /**
+ * Jexer - Java Text User Interface library
+ *
+ * <p>
+ * This library is a text-based windowing system loosely reminiscent of
+ * Borland's <a href="http://en.wikipedia.org/wiki/Turbo_Vision">Turbo
+ * Vision</a> library. Jexer's goal is to enable people to get up and
+ * running with minimum hassle and lots of polish. A very quick "Hello
+ * World" application can be created as simply as this:
+ *
+ * <pre>
+ * {@code
+ * import jexer.TApplication;
+ *
+ * public class MyApplication extends TApplication {
+ *
+ * public MyApplication() throws Exception {
+ * super(BackendType.XTERM);
+ *
+ * // Create standard menus for Tool, File, and Window.
+ * addToolMenu();
+ * addFileMenu();
+ * addWindowMenu();
+ * }
+ *
+ * public static void main(String [] args) throws Exception {
+ * MyApplication app = new MyApplication();
+ * app.run();
+ * }
+ * }
+ * }
+ * </pre>
+ */
+ package jexer;
--- /dev/null
+ Copyright (c) 2010 Dimitar Toshkov Zhekov,\r
+ with Reserved Font Name "Terminus Font".\r
+ \r
+ Copyright (c) 2011 Tilman Blumenbach,\r
+ with Reserved Font Name "Terminus (TTF)".\r
+ \r
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.\r
+ This license is copied below, and is also available with a FAQ at:\r
+ http://scripts.sil.org/OFL\r
+ \r
+ \r
+ -----------------------------------------------------------\r
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\r
+ -----------------------------------------------------------\r
+ \r
+ PREAMBLE\r
+ The goals of the Open Font License (OFL) are to stimulate worldwide\r
+ development of collaborative font projects, to support the font creation\r
+ efforts of academic and linguistic communities, and to provide a free and\r
+ open framework in which fonts may be shared and improved in partnership\r
+ with others.\r
+ \r
+ The OFL allows the licensed fonts to be used, studied, modified and\r
+ redistributed freely as long as they are not sold by themselves. The\r
+ fonts, including any derivative works, can be bundled, embedded, \r
+ redistributed and/or sold with any software provided that any reserved\r
+ names are not used by derivative works. The fonts and derivatives,\r
+ however, cannot be released under any other type of license. The\r
+ requirement for fonts to remain under this license does not apply\r
+ to any document created using the fonts or their derivatives.\r
+ \r
+ DEFINITIONS\r
+ "Font Software" refers to the set of files released by the Copyright\r
+ Holder(s) under this license and clearly marked as such. This may\r
+ include source files, build scripts and documentation.\r
+ \r
+ "Reserved Font Name" refers to any names specified as such after the\r
+ copyright statement(s).\r
+ \r
+ "Original Version" refers to the collection of Font Software components as\r
+ distributed by the Copyright Holder(s).\r
+ \r
+ "Modified Version" refers to any derivative made by adding to, deleting,\r
+ or substituting -- in part or in whole -- any of the components of the\r
+ Original Version, by changing formats or by porting the Font Software to a\r
+ new environment.\r
+ \r
+ "Author" refers to any designer, engineer, programmer, technical\r
+ writer or other person who contributed to the Font Software.\r
+ \r
+ PERMISSION & CONDITIONS\r
+ Permission is hereby granted, free of charge, to any person obtaining\r
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,\r
+ redistribute, and sell modified and unmodified copies of the Font\r
+ Software, subject to the following conditions:\r
+ \r
+ 1) Neither the Font Software nor any of its individual components,\r
+ in Original or Modified Versions, may be sold by itself.\r
+ \r
+ 2) Original or Modified Versions of the Font Software may be bundled,\r
+ redistributed and/or sold with any software, provided that each copy\r
+ contains the above copyright notice and this license. These can be\r
+ included either as stand-alone text files, human-readable headers or\r
+ in the appropriate machine-readable metadata fields within text or\r
+ binary files as long as those fields can be easily viewed by the user.\r
+ \r
+ 3) No Modified Version of the Font Software may use the Reserved Font\r
+ Name(s) unless explicit written permission is granted by the corresponding\r
+ Copyright Holder. This restriction only applies to the primary font name as\r
+ presented to the users.\r
+ \r
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\r
+ Software shall not be used to promote, endorse or advertise any\r
+ Modified Version, except to acknowledge the contribution(s) of the\r
+ Copyright Holder(s) and the Author(s) or with their explicit written\r
+ permission.\r
+ \r
+ 5) The Font Software, modified or unmodified, in part or in whole,\r
+ must be distributed entirely under this license, and must not be\r
+ distributed under any other license. The requirement for fonts to\r
+ remain under this license does not apply to any document created\r
+ using the Font Software.\r
+ \r
+ TERMINATION\r
+ This license becomes null and void if any of the above conditions are\r
+ not met.\r
+ \r
+ DISCLAIMER\r
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,\r
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\r
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\r
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\r
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\r
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\r
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\r
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\r
+ OTHER DEALINGS IN THE FONT SOFTWARE.\r
--- /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.teditor;
+
+ import java.io.FileOutputStream;
+ import java.io.IOException;
+ import java.io.OutputStreamWriter;
+ import java.util.ArrayList;
+ import java.util.List;
+
+ import jexer.bits.CellAttributes;
+
+ /**
+ * A Document represents a text file, as a collection of lines.
+ */
+ public class Document {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The list of lines.
+ */
+ private ArrayList<Line> lines = new ArrayList<Line>();
+
+ /**
+ * The current line number being edited. Note that this is 0-based, the
+ * first line is line number 0.
+ */
+ private int lineNumber = 0;
+
+ /**
+ * The overwrite flag. When true, characters overwrite data.
+ */
+ private boolean overwrite = false;
+
+ /**
+ * If true, the document has been edited.
+ */
+ private boolean dirty = false;
+
+ /**
+ * The default color for the TEditor class.
+ */
+ private CellAttributes defaultColor = null;
+
+ /**
+ * The text highlighter to use.
+ */
+ private Highlighter highlighter = new Highlighter();
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Construct a new Document from an existing text string.
+ *
+ * @param str the text string
+ * @param defaultColor the color for unhighlighted text
+ */
+ public Document(final String str, final CellAttributes defaultColor) {
+ this.defaultColor = defaultColor;
+
+ // TODO: set different colors based on file extension
+ highlighter.setJavaColors();
+
+ String [] rawLines = str.split("\n");
+ for (int i = 0; i < rawLines.length; i++) {
+ lines.add(new Line(rawLines[i], this.defaultColor, highlighter));
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // Document ---------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get the overwrite flag.
+ *
+ * @return true if addChar() overwrites data, false if it inserts
+ */
+ public boolean getOverwrite() {
+ return overwrite;
+ }
+
+ /**
+ * Get the dirty value.
+ *
+ * @return true if the buffer is dirty
+ */
+ public boolean isDirty() {
+ return dirty;
+ }
+
+ /**
+ * Save contents to file.
+ *
+ * @param filename file to save to
+ * @throws IOException if a java.io operation throws
+ */
+ public void saveToFilename(final String filename) throws IOException {
+ OutputStreamWriter output = null;
+ try {
+ output = new OutputStreamWriter(new FileOutputStream(filename),
+ "UTF-8");
+
+ for (Line line: lines) {
+ output.write(line.getRawString());
+ output.write("\n");
+ }
+
+ dirty = false;
+ }
+ finally {
+ if (output != null) {
+ output.close();
+ }
+ }
+ }
+
+ /**
+ * Set the overwrite flag.
+ *
+ * @param overwrite true if addChar() should overwrite data, false if it
+ * should insert
+ */
+ public void setOverwrite(final boolean overwrite) {
+ this.overwrite = overwrite;
+ }
+
+ /**
+ * Get the current line number being edited.
+ *
+ * @return the line number. Note that this is 0-based: 0 is the first
+ * line.
+ */
+ public int getLineNumber() {
+ return lineNumber;
+ }
+
+ /**
+ * Get the current editing line.
+ *
+ * @return the line
+ */
+ public Line getCurrentLine() {
+ return lines.get(lineNumber);
+ }
+
+ /**
+ * Get a specific line by number.
+ *
+ * @param lineNumber the line number. Note that this is 0-based: 0 is
+ * the first line.
+ * @return the line
+ */
+ public Line getLine(final int lineNumber) {
+ return lines.get(lineNumber);
+ }
+
+ /**
+ * Set the current line number being edited.
+ *
+ * @param n the line number. Note that this is 0-based: 0 is the first
+ * line.
+ */
+ public void setLineNumber(final int n) {
+ if ((n < 0) || (n > lines.size())) {
+ throw new IndexOutOfBoundsException("Lines array size is " +
+ lines.size() + ", requested index " + n);
+ }
+ lineNumber = n;
+ }
+
+ /**
+ * Get the current cursor position of the editing line.
+ *
+ * @return the cursor position
+ */
+ public int getCursor() {
+ return lines.get(lineNumber).getCursor();
+ }
+
+ /**
+ * Get the character at the current cursor position in the text.
+ *
+ * @return the character, or -1 if the cursor is at the end of the line
+ */
+ public int getChar() {
+ return lines.get(lineNumber).getChar();
+ }
+
+ /**
+ * Set the current cursor position of the editing line. 0-based.
+ *
+ * @param cursor the new cursor position
+ */
+ public void setCursor(final int cursor) {
+ if (cursor >= lines.get(lineNumber).getDisplayLength()) {
+ lines.get(lineNumber).end();
+ } else {
+ lines.get(lineNumber).setCursor(cursor);
+ }
+ }
+
+ /**
+ * Increment the line number by one. If at the last line, do nothing.
+ *
+ * @return true if the editing line changed
+ */
+ public boolean down() {
+ if (lineNumber < lines.size() - 1) {
+ int x = lines.get(lineNumber).getCursor();
+ lineNumber++;
+ if (x >= lines.get(lineNumber).getDisplayLength()) {
+ lines.get(lineNumber).end();
+ } else {
+ lines.get(lineNumber).setCursor(x);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Increment the line number by n. If n would go past the last line,
+ * increment only to the last line.
+ *
+ * @param n the number of lines to increment by
+ * @return true if the editing line changed
+ */
+ public boolean down(final int n) {
+ if (lineNumber < lines.size() - 1) {
+ int x = lines.get(lineNumber).getCursor();
+ lineNumber += n;
+ if (lineNumber > lines.size() - 1) {
+ lineNumber = lines.size() - 1;
+ }
+ if (x >= lines.get(lineNumber).getDisplayLength()) {
+ lines.get(lineNumber).end();
+ } else {
+ lines.get(lineNumber).setCursor(x);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Decrement the line number by one. If at the first line, do nothing.
+ *
+ * @return true if the editing line changed
+ */
+ public boolean up() {
+ if (lineNumber > 0) {
+ int x = lines.get(lineNumber).getCursor();
+ lineNumber--;
+ if (x >= lines.get(lineNumber).getDisplayLength()) {
+ lines.get(lineNumber).end();
+ } else {
+ lines.get(lineNumber).setCursor(x);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Decrement the line number by n. If n would go past the first line,
+ * decrement only to the first line.
+ *
+ * @param n the number of lines to decrement by
+ * @return true if the editing line changed
+ */
+ public boolean up(final int n) {
+ if (lineNumber > 0) {
+ int x = lines.get(lineNumber).getCursor();
+ lineNumber -= n;
+ if (lineNumber < 0) {
+ lineNumber = 0;
+ }
+ if (x >= lines.get(lineNumber).getDisplayLength()) {
+ lines.get(lineNumber).end();
+ } else {
+ lines.get(lineNumber).setCursor(x);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Decrement the cursor by one. If at the first column on the first
+ * line, do nothing.
+ *
+ * @return true if the cursor position changed
+ */
+ public boolean left() {
+ if (!lines.get(lineNumber).left()) {
+ // We are on the leftmost column, wrap
+ if (up()) {
+ end();
+ } else {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Increment the cursor by one. If at the last column on the last line,
+ * do nothing.
+ *
+ * @return true if the cursor position changed
+ */
+ public boolean right() {
+ if (!lines.get(lineNumber).right()) {
+ // We are on the rightmost column, wrap
+ if (down()) {
+ home();
+ } else {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Go back to the beginning of this word if in the middle, or the
+ * beginning of the previous word.
+ */
+ public void backwardsWord() {
+
+ // If at the beginning of a word already, push past it.
+ if ((getChar() != -1)
+ && (getRawLine().length() > 0)
+ && !Character.isSpace((char) getChar())
+ ) {
+ left();
+ }
+
+ // int line = lineNumber;
+ while ((getChar() == -1)
+ || (getRawLine().length() == 0)
+ || Character.isSpace((char) getChar())
+ ) {
+ if (left() == false) {
+ return;
+ }
+ }
+
+
+ assert (getChar() != -1);
+
+ if (!Character.isSpace((char) getChar())
+ && (getRawLine().length() > 0)
+ ) {
+ // Advance until at the beginning of the document or a whitespace
+ // is encountered.
+ while (!Character.isSpace((char) getChar())) {
+ int line = lineNumber;
+ if (left() == false) {
+ // End of document, bail out.
+ return;
+ }
+ if (lineNumber != line) {
+ // We wrapped a line. Here that counts as whitespace.
+ right();
+ return;
+ }
+ }
+ }
+
+ // We went one past the word, push back to the first character of
+ // that word.
+ right();
+ return;
+ }
+
+ /**
+ * Go to the beginning of the next word.
+ */
+ public void forwardsWord() {
+ int line = lineNumber;
+ while ((getChar() == -1)
+ || (getRawLine().length() == 0)
+ ) {
+ if (right() == false) {
+ return;
+ }
+ if (lineNumber != line) {
+ // We wrapped a line. Here that counts as whitespace.
+ if (!Character.isSpace((char) getChar())) {
+ // We found a character immediately after the line.
+ // Done!
+ return;
+ }
+ // Still looking...
+ line = lineNumber;
+ }
+ }
+ assert (getChar() != -1);
+
+ if (!Character.isSpace((char) getChar())
+ && (getRawLine().length() > 0)
+ ) {
+ // Advance until at the end of the document or a whitespace is
+ // encountered.
+ while (!Character.isSpace((char) getChar())) {
+ line = lineNumber;
+ if (right() == false) {
+ // End of document, bail out.
+ return;
+ }
+ if (lineNumber != line) {
+ // We wrapped a line. Here that counts as whitespace.
+ if (!Character.isSpace((char) getChar())
+ && (getRawLine().length() > 0)
+ ) {
+ // We found a character immediately after the line.
+ // Done!
+ return;
+ }
+ break;
+ }
+ }
+ }
+
+ while ((getChar() == -1)
+ || (getRawLine().length() == 0)
+ ) {
+ if (right() == false) {
+ return;
+ }
+ if (lineNumber != line) {
+ // We wrapped a line. Here that counts as whitespace.
+ if (!Character.isSpace((char) getChar())) {
+ // We found a character immediately after the line.
+ // Done!
+ return;
+ }
+ // Still looking...
+ line = lineNumber;
+ }
+ }
+ assert (getChar() != -1);
+
+ if (Character.isSpace((char) getChar())) {
+ // Advance until at the end of the document or a non-whitespace
+ // is encountered.
+ while (Character.isSpace((char) getChar())) {
+ if (right() == false) {
+ // End of document, bail out.
+ return;
+ }
+ }
+ return;
+ }
+
+ // We wrapped the line to get here.
+ return;
+ }
+
+ /**
+ * Get the raw string that matches this line.
+ *
+ * @return the string
+ */
+ public String getRawLine() {
+ return lines.get(lineNumber).getRawString();
+ }
+
+ /**
+ * Go to the first column of this line.
+ *
+ * @return true if the cursor position changed
+ */
+ public boolean home() {
+ return lines.get(lineNumber).home();
+ }
+
+ /**
+ * Go to the last column of this line.
+ *
+ * @return true if the cursor position changed
+ */
+ public boolean end() {
+ return lines.get(lineNumber).end();
+ }
+
+ /**
+ * Delete the character under the cursor.
+ */
+ public void del() {
+ dirty = true;
+ int cursor = lines.get(lineNumber).getCursor();
+ if (cursor < lines.get(lineNumber).getDisplayLength() - 1) {
+ lines.get(lineNumber).del();
+ } else if (lineNumber < lines.size() - 2) {
+ // Join two lines
+ StringBuilder newLine = new StringBuilder(lines.
+ get(lineNumber).getRawString());
+ newLine.append(lines.get(lineNumber + 1).getRawString());
+ lines.set(lineNumber, new Line(newLine.toString(),
+ defaultColor, highlighter));
+ lines.get(lineNumber).setCursor(cursor);
+ lines.remove(lineNumber + 1);
+ }
+ }
+
+ /**
+ * Delete the character immediately preceeding the cursor.
+ */
+ public void backspace() {
+ dirty = true;
+ int cursor = lines.get(lineNumber).getCursor();
+ if (cursor > 0) {
+ lines.get(lineNumber).backspace();
+ } else if (lineNumber > 0) {
+ // Join two lines
+ lineNumber--;
+ String firstLine = lines.get(lineNumber).getRawString();
+ if (firstLine.length() > 0) {
+ // Backspacing combining two lines
+ StringBuilder newLine = new StringBuilder(firstLine);
+ newLine.append(lines.get(lineNumber + 1).getRawString());
+ lines.set(lineNumber, new Line(newLine.toString(),
+ defaultColor, highlighter));
+ lines.get(lineNumber).setCursor(firstLine.length());
+ lines.remove(lineNumber + 1);
+ } else {
+ // Backspacing an empty line
+ lines.remove(lineNumber);
+ lines.get(lineNumber).setCursor(0);
+ }
+ }
+ }
+
+ /**
+ * Split the current line into two, like pressing the enter key.
+ */
+ public void enter() {
+ dirty = true;
+ int cursor = lines.get(lineNumber).getRawCursor();
+ String original = lines.get(lineNumber).getRawString();
+ String firstLine = original.substring(0, cursor);
+ String secondLine = original.substring(cursor);
+ lines.add(lineNumber + 1, new Line(secondLine, defaultColor,
+ highlighter));
+ lines.set(lineNumber, new Line(firstLine, defaultColor, highlighter));
+ lineNumber++;
+ lines.get(lineNumber).home();
+ }
+
+ /**
+ * Replace or insert a character at the cursor, depending on overwrite
+ * flag.
+ *
+ * @param ch the character to replace or insert
+ */
+ public void addChar(final int ch) {
+ dirty = true;
+ if (overwrite) {
+ lines.get(lineNumber).replaceChar(ch);
+ } else {
+ lines.get(lineNumber).addChar(ch);
+ }
+ }
+
+ /**
+ * Get a (shallow) copy of the list of lines.
+ *
+ * @return the list of lines
+ */
+ public List<Line> getLines() {
+ return new ArrayList<Line>(lines);
+ }
+
+ /**
+ * Get the number of lines.
+ *
+ * @return the number of lines
+ */
+ public int getLineCount() {
+ return lines.size();
+ }
+
+ /**
+ * Compute the maximum line length for this document.
+ *
+ * @return the number of cells needed to display the longest line
+ */
+ public int getLineLengthMax() {
+ int n = 0;
+ for (Line line : lines) {
+ if (line.getDisplayLength() > n) {
+ n = line.getDisplayLength();
+ }
+ }
+ return n;
+ }
+
+ /**
+ * Get the current line length.
+ *
+ * @return the number of cells needed to display the current line
+ */
+ public int getLineLength() {
+ return lines.get(lineNumber).getDisplayLength();
+ }
+
+ }
--- /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.teditor;
+
+ import java.util.SortedMap;
+ import java.util.TreeMap;
+
+ import jexer.bits.CellAttributes;
+ import jexer.bits.Color;
+
+ /**
+ * Highlighter provides color choices for certain text strings.
+ */
+ public class Highlighter {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The highlighter colors.
+ */
+ private SortedMap<String, CellAttributes> colors;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor sets the theme to the default.
+ */
+ public Highlighter() {
+ colors = new TreeMap<String, CellAttributes>();
+ }
+
+ // ------------------------------------------------------------------------
+ // Highlighter ------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * See if this is a character that should split a word.
+ *
+ * @param ch the character
+ * @return true if the word should be split
+ */
+ public boolean shouldSplit(final int ch) {
+ // For now, split on punctuation
+ String punctuation = "'\"\\<>{}[]!@#$%^&*();:.,-+/*?";
+ if (ch < 0x100) {
+ if (punctuation.indexOf((char) ch) != -1) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Retrieve the CellAttributes for a named theme color.
+ *
+ * @param name theme color name, e.g. "twindow.border"
+ * @return color associated with name, e.g. bold yellow on blue
+ */
+ public CellAttributes getColor(final String name) {
+ CellAttributes attr = (CellAttributes) colors.get(name);
+ return attr;
+ }
+
+ /**
+ * Sets to defaults that resemble the Borland IDE colors.
+ */
+ public void setJavaColors() {
+ CellAttributes color;
+
+ String [] keywords = {
+ "boolean", "byte", "short", "int", "long", "char", "float",
+ "double", "void", "new",
+ "static", "final", "volatile", "synchronized", "abstract",
+ "public", "private", "protected",
+ "class", "interface", "extends", "implements",
+ "if", "else", "do", "while", "for", "break", "continue",
+ "switch", "case", "default",
+ };
+ color = new CellAttributes();
+ color.setForeColor(Color.WHITE);
+ color.setBackColor(Color.BLUE);
+ color.setBold(true);
+ for (String str: keywords) {
+ colors.put(str, color);
+ }
+
+ String [] operators = {
+ "[", "]", "(", ")", "{", "}",
+ "*", "-", "+", "/", "=", "%",
+ "^", "&", "!", "<<", ">>", "<<<", ">>>",
+ "&&", "||",
+ ">", "<", ">=", "<=", "!=", "==",
+ ",", ";", ".", "?", ":",
+ };
+ color = new CellAttributes();
+ color.setForeColor(Color.CYAN);
+ color.setBackColor(Color.BLUE);
+ color.setBold(true);
+ for (String str: operators) {
+ colors.put(str, color);
+ }
+
+ String [] packageKeywords = {
+ "package", "import",
+ };
+ color = new CellAttributes();
+ color.setForeColor(Color.GREEN);
+ color.setBackColor(Color.BLUE);
+ color.setBold(true);
+ for (String str: packageKeywords) {
+ colors.put(str, color);
+ }
+
+ }
+
+ }
--- /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.teditor;
+
+ import java.util.ArrayList;
+ import java.util.List;
+
+ import jexer.bits.CellAttributes;
+ import jexer.bits.StringUtils;
+
+ /**
+ * A Line represents a single line of text on the screen, as a collection of
+ * words.
+ */
+ public class Line {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The list of words.
+ */
+ private ArrayList<Word> words = new ArrayList<Word>();
+
+ /**
+ * The default color for the TEditor class.
+ */
+ private CellAttributes defaultColor = null;
+
+ /**
+ * The text highlighter to use.
+ */
+ private Highlighter highlighter = null;
+
+ /**
+ * The current edition position on this line.
+ */
+ private int position = 0;
+
+ /**
+ * The current editing position screen column number.
+ */
+ private int screenPosition = 0;
+
+ /**
+ * The raw text of this line, what is passed to Word to determine
+ * highlighting behavior.
+ */
+ private StringBuilder rawText;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Construct a new Line from an existing text string, and highlight
+ * certain strings.
+ *
+ * @param str the text string
+ * @param defaultColor the color for unhighlighted text
+ * @param highlighter the highlighter to use
+ */
+ public Line(final String str, final CellAttributes defaultColor,
+ final Highlighter highlighter) {
+
+ this.defaultColor = defaultColor;
+ this.highlighter = highlighter;
+ this.rawText = new StringBuilder(str);
+
+ scanLine();
+ }
+
+ /**
+ * Construct a new Line from an existing text string.
+ *
+ * @param str the text string
+ * @param defaultColor the color for unhighlighted text
+ */
+ public Line(final String str, final CellAttributes defaultColor) {
+ this(str, defaultColor, null);
+ }
+
+ // ------------------------------------------------------------------------
+ // Line -------------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get a (shallow) copy of the words in this line.
+ *
+ * @return a copy of the word list
+ */
+ public List<Word> getWords() {
+ return new ArrayList<Word>(words);
+ }
+
+ /**
+ * Get the current cursor position in the text.
+ *
+ * @return the cursor position
+ */
+ public int getRawCursor() {
+ return position;
+ }
+
+ /**
+ * Get the current cursor position on screen.
+ *
+ * @return the cursor position
+ */
+ public int getCursor() {
+ return screenPosition;
+ }
+
+ /**
+ * Set the current cursor position.
+ *
+ * @param cursor the new cursor position
+ */
+ public void setCursor(final int cursor) {
+ if ((cursor < 0)
+ || ((cursor >= getDisplayLength())
+ && (getDisplayLength() > 0))
+ ) {
+ throw new IndexOutOfBoundsException("Max length is " +
+ getDisplayLength() + ", requested position " + cursor);
+ }
+ screenPosition = cursor;
+ position = screenToTextPosition(screenPosition);
+ }
+
+ /**
+ * Get the character at the current cursor position in the text.
+ *
+ * @return the character, or -1 if the cursor is at the end of the line
+ */
+ public int getChar() {
+ if (position == rawText.length()) {
+ return -1;
+ }
+ return rawText.codePointAt(position);
+ }
+
+ /**
+ * Get the on-screen display length.
+ *
+ * @return the number of cells needed to display this line
+ */
+ public int getDisplayLength() {
+ int n = StringUtils.width(rawText.toString());
+
+ if (n > 0) {
+ // If we have any visible characters, add one to the display so
+ // that the position is immediately after the data.
+ return n + 1;
+ }
+ return n;
+ }
+
+ /**
+ * Get the raw string that matches this line.
+ *
+ * @return the string
+ */
+ public String getRawString() {
+ return rawText.toString();
+ }
+
+ /**
+ * Scan rawText and make words out of it.
+ */
+ private void scanLine() {
+ words.clear();
+ Word word = new Word(this.defaultColor, this.highlighter);
+ words.add(word);
+ for (int i = 0; i < rawText.length();) {
+ int ch = rawText.codePointAt(i);
+ i += Character.charCount(ch);
+ Word newWord = word.addChar(ch);
+ if (newWord != word) {
+ words.add(newWord);
+ word = newWord;
+ }
+ }
+ for (Word w: words) {
+ w.applyHighlight();
+ }
+ }
+
+ /**
+ * Decrement the cursor by one. If at the first column, do nothing.
+ *
+ * @return true if the cursor position changed
+ */
+ public boolean left() {
+ if (position == 0) {
+ return false;
+ }
+ screenPosition -= StringUtils.width(rawText.codePointBefore(position));
+ position -= Character.charCount(rawText.codePointBefore(position));
+ return true;
+ }
+
+ /**
+ * Increment the cursor by one. If at the last column, do nothing.
+ *
+ * @return true if the cursor position changed
+ */
+ public boolean right() {
+ if (getDisplayLength() == 0) {
+ return false;
+ }
+ if (position == getDisplayLength() - 1) {
+ return false;
+ }
+ if (position < rawText.length()) {
+ screenPosition += StringUtils.width(rawText.codePointAt(position));
+ position += Character.charCount(rawText.codePointAt(position));
+ }
+ assert (position <= rawText.length());
+ return true;
+ }
+
+ /**
+ * Go to the first column of this line.
+ *
+ * @return true if the cursor position changed
+ */
+ public boolean home() {
+ if (position > 0) {
+ position = 0;
+ screenPosition = 0;
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Go to the last column of this line.
+ *
+ * @return true if the cursor position changed
+ */
+ public boolean end() {
+ if (position != getDisplayLength() - 1) {
+ position = rawText.length();
+ screenPosition = StringUtils.width(rawText.toString());
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Delete the character under the cursor.
+ */
+ public void del() {
+ assert (words.size() > 0);
+
+ if (position < getDisplayLength()) {
+ int n = Character.charCount(rawText.codePointAt(position));
+ for (int i = 0; i < n; i++) {
+ rawText.deleteCharAt(position);
+ }
+ }
+
+ // Re-scan the line to determine the new word boundaries.
+ scanLine();
+ }
+
+ /**
+ * Delete the character immediately preceeding the cursor.
+ */
+ public void backspace() {
+ if (left()) {
+ del();
+ }
+ }
+
+ /**
+ * Insert a character at the cursor.
+ *
+ * @param ch the character to insert
+ */
+ public void addChar(final int ch) {
+ if (position < getDisplayLength() - 1) {
+ rawText.insert(position, Character.toChars(ch));
+ } else {
+ rawText.append(Character.toChars(ch));
+ }
+ position += Character.charCount(ch);
+ screenPosition += StringUtils.width(ch);
+ scanLine();
+ }
+
+ /**
+ * Replace a character at the cursor.
+ *
+ * @param ch the character to replace
+ */
+ public void replaceChar(final int ch) {
+ if (position < getDisplayLength() - 1) {
+ // Replace character
+ String oldText = rawText.toString();
+ rawText = new StringBuilder(oldText.substring(0, position));
+ rawText.append(Character.toChars(ch));
+ rawText.append(oldText.substring(position + 1));
+ screenPosition += StringUtils.width(rawText.codePointAt(position));
+ position += Character.charCount(ch);
+ } else {
+ rawText.append(Character.toChars(ch));
+ position += Character.charCount(ch);
+ screenPosition += StringUtils.width(ch);
+ }
+ scanLine();
+ }
+
+ /**
+ * Determine string position from screen position.
+ *
+ * @param screenPosition the position on screen
+ * @return the equivalent position in text
+ */
+ protected int screenToTextPosition(final int screenPosition) {
+ if (screenPosition == 0) {
+ return 0;
+ }
+
+ int n = 0;
+ for (int i = 0; i < rawText.length(); i++) {
+ n += StringUtils.width(rawText.codePointAt(i));
+ if (n >= screenPosition) {
+ return i + 1;
+ }
+ }
+ // screenPosition exceeds the available text length.
+ throw new IndexOutOfBoundsException("screenPosition " + screenPosition +
+ " exceeds available text length " + rawText.length());
+ }
+
+ }
--- /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.teditor;
+
+ import jexer.bits.CellAttributes;
+ import jexer.bits.StringUtils;
+
+ /**
+ * A Word represents text that was entered by the user. It can be either
+ * whitespace or non-whitespace.
+ *
+ * Very dumb highlighting is supported, it has no sense of parsing (not even
+ * comments). For now this only highlights some Java keywords and
+ * puctuation.
+ */
+ public class Word {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The color to render this word as on screen.
+ */
+ private CellAttributes color = new CellAttributes();
+
+ /**
+ * The default color for the TEditor class.
+ */
+ private CellAttributes defaultColor = null;
+
+ /**
+ * The text highlighter to use.
+ */
+ private Highlighter highlighter = null;
+
+ /**
+ * The actual text of this word. Average word length is 6 characters,
+ * with a lot of shorter ones, so start with 3.
+ */
+ private StringBuilder text = new StringBuilder(3);
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Construct a word with one character.
+ *
+ * @param ch the first character of the word
+ * @param defaultColor the color for unhighlighted text
+ * @param highlighter the highlighter to use
+ */
+ public Word(final int ch, final CellAttributes defaultColor,
+ final Highlighter highlighter) {
+
+ this.defaultColor = defaultColor;
+ this.highlighter = highlighter;
+ text.append(Character.toChars(ch));
+ }
+
+ /**
+ * Construct a word with an empty string.
+ *
+ * @param defaultColor the color for unhighlighted text
+ * @param highlighter the highlighter to use
+ */
+ public Word(final CellAttributes defaultColor,
+ final Highlighter highlighter) {
+
+ this.defaultColor = defaultColor;
+ this.highlighter = highlighter;
+ }
+
+ // ------------------------------------------------------------------------
+ // Word -------------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get the color used to display this word on screen.
+ *
+ * @return the color
+ */
+ public CellAttributes getColor() {
+ return new CellAttributes(color);
+ }
+
+ /**
+ * Set the color used to display this word on screen.
+ *
+ * @param color the color
+ */
+ public void setColor(final CellAttributes color) {
+ color.setTo(color);
+ }
+
+ /**
+ * Get the text to display.
+ *
+ * @return the text
+ */
+ public String getText() {
+ return text.toString();
+ }
+
+ /**
+ * Get the on-screen display length.
+ *
+ * @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());
+ }
+
+ /**
+ * See if this is a whitespace word. Note that empty string is
+ * considered whitespace.
+ *
+ * @return true if this word is whitespace
+ */
+ public boolean isWhitespace() {
+ if (text.length() == 0) {
+ return true;
+ }
+ if (Character.isWhitespace(text.charAt(0))) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Perform highlighting.
+ */
+ public void applyHighlight() {
+ color.setTo(defaultColor);
+ if (highlighter == null) {
+ return;
+ }
+ String key = text.toString();
+ CellAttributes newColor = highlighter.getColor(key);
+ if (newColor != null) {
+ color.setTo(newColor);
+ }
+ }
+
+ /**
+ * Add a character to this word. If this is a whitespace character
+ * adding to a non-whitespace word, create a new word and return that;
+ * similarly if this a non-whitespace character adding to a whitespace
+ * word, create a new word and return that. Note package private access:
+ * this is only called by Line to figure out highlighting boundaries.
+ *
+ * @param ch the new character to add
+ * @return either this word (if it was added), or a new word that
+ * contains ch
+ */
+ public Word addChar(final int ch) {
+ if (text.length() == 0) {
+ text.append(Character.toChars(ch));
+ return this;
+ }
+
+ // Give the highlighter the option to split here.
+ if (highlighter != null) {
+ if (highlighter.shouldSplit(ch)
+ || highlighter.shouldSplit(text.charAt(0))
+ ) {
+ Word newWord = new Word(ch, defaultColor, highlighter);
+ return newWord;
+ }
+ }
+
+ // Highlighter didn't care, so split at whitespace.
+ if (Character.isWhitespace(text.charAt(0))
+ && Character.isWhitespace(ch)
+ ) {
+ // Adding to a whitespace word, keep at it.
+ text.append(Character.toChars(ch));
+ return this;
+ }
+ if (!Character.isWhitespace(text.charAt(0))
+ && !Character.isWhitespace(ch)
+ ) {
+ // Adding to a non-whitespace word, keep at it.
+ text.append(Character.toChars(ch));
+ return this;
+ }
+
+ // Switching from whitespace to non-whitespace or vice versa, so
+ // split here.
+ Word newWord = new Word(ch, defaultColor, highlighter);
+ return newWord;
+ }
+
+ }
--- /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
+ */
+
+ /**
+ * A basic text editor backend supporting word highlighting.
+ */
+ package jexer.teditor;
--- /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.tterminal;
+
+ /**
+ * This class contains a collection of the DEC VT100 and VT220 character set
+ * mappings into Unicode.
+ */
+ public final class DECCharacterSets {
+
+ // ------------------------------------------------------------------------
+ // Constants --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * US - Normal "international" (ASCII).
+ */
+ public static final char [] US_ASCII = {
+ 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007,
+ 0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
+ 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017,
+ 0x0018, 0x0019, 0x001A, 0x001B, 0x001C, 0x001D, 0x001E, 0x001F,
+ 0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027,
+ 0x0028, 0x0029, 0x002A, 0x002B, 0x002C, 0x002D, 0x002E, 0x002F,
+ 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
+ 0x0038, 0x0039, 0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F,
+ 0x0040, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
+ 0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F,
+ 0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057,
+ 0x0058, 0x0059, 0x005A, 0x005B, 0x005C, 0x005D, 0x005E, 0x005F,
+ 0x0060, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
+ 0x0068, 0x0069, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F,
+ 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077,
+ 0x0078, 0x0079, 0x007A, 0x007B, 0x007C, 0x007D, 0x007E, 0x0020
+ };
+
+ /**
+ * DEC Supplemental Graphics (VT100 drawing characters).
+ */
+ public static final char [] SPECIAL_GRAPHICS = {
+ 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007,
+ 0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
+ 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017,
+ 0x0018, 0x0019, 0x001A, 0x001B, 0x001C, 0x001D, 0x001E, 0x001F,
+ 0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027,
+ 0x0028, 0x0029, 0x002A, 0x002B, 0x002C, 0x002D, 0x002E, 0x002F,
+ 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
+ 0x0038, 0x0039, 0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F,
+ 0x0040, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
+ 0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F,
+ 0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057,
+ 0x0058, 0x0059, 0x005A, 0x005B, 0x005C, 0x005D, 0x005E, 0x005F,
+ 0x2666, 0x2592, 0x2409, 0x240C, 0x240D, 0x240A, 0x00B0, 0x00B1,
+ 0x2424, 0x240B, 0x2518, 0x2510, 0x250C, 0x2514, 0x253C, 0x23BA,
+ 0x23BB, 0x2500, 0x23BC, 0x23BD, 0x251C, 0x2524, 0x2534, 0x252C,
+ 0x2502, 0x2264, 0x2265, 0x03C0, 0x2260, 0x00A3, 0x00B7, 0x0020
+ };
+
+ /**
+ * Dec Supplemental (DEC multinational).
+ */
+ public static final char [] DEC_SUPPLEMENTAL = {
+ 0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086, 0x0087,
+ 0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x008D, 0x008E, 0x008F,
+ 0x0090, 0x0091, 0x0092, 0x0093, 0x0094, 0x0095, 0x0096, 0x0097,
+ 0x0098, 0x0099, 0x009A, 0x009B, 0x009C, 0x009D, 0x009E, 0x009F,
+ 0x0020, 0x00A1, 0x00A2, 0x00A3, 0x00A8, 0x00A5, 0x0020, 0x00A7,
+ 0x00A4, 0x00A9, 0x00AA, 0x00AB, 0x0020, 0x0020, 0x0020, 0x0020,
+ 0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x0020, 0x00B5, 0x00B6, 0x00B7,
+ 0x0020, 0x00B9, 0x00BA, 0x00BB, 0x00BC, 0x00BD, 0x0020, 0x00BF,
+ 0x00C0, 0x00C1, 0x00C2, 0x00C3, 0x00C4, 0x00C5, 0x00C6, 0x00C7,
+ 0x00C8, 0x00C9, 0x00CA, 0x00CB, 0x00CC, 0x00CD, 0x00CE, 0x00CF,
+ 0x0020, 0x00D1, 0x00D2, 0x00D3, 0x00D4, 0x00D5, 0x00D6, 0x0157,
+ 0x00D8, 0x00D9, 0x00DA, 0x00DB, 0x00DC, 0x0178, 0x0020, 0x00DF,
+ 0x00E0, 0x00E1, 0x00E2, 0x00E3, 0x00E4, 0x00E5, 0x00E6, 0x00E7,
+ 0x00E8, 0x00E9, 0x00EA, 0x00EB, 0x00EC, 0x00ED, 0x00EE, 0x00EF,
+ 0x0020, 0x00F1, 0x00F2, 0x00F3, 0x00F4, 0x00F5, 0x00F6, 0x0153,
+ 0x00F8, 0x00F9, 0x00FA, 0x00FB, 0x00FC, 0x00FF, 0x0020, 0x0020
+ };
+
+ /**
+ * UK.
+ */
+ public static final char [] UK = {
+ 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007,
+ 0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
+ 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017,
+ 0x0018, 0x0019, 0x001A, 0x001B, 0x001C, 0x001D, 0x001E, 0x001F,
+ 0x0020, 0x0021, 0x0022, 0x00A3, 0x0024, 0x0025, 0x0026, 0x0027,
+ 0x0028, 0x0029, 0x002A, 0x002B, 0x002C, 0x002D, 0x002E, 0x002F,
+ 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
+ 0x0038, 0x0039, 0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F,
+ 0x0040, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
+ 0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F,
+ 0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057,
+ 0x0058, 0x0059, 0x005A, 0x005B, 0x005C, 0x005D, 0x005E, 0x005F,
+ 0x0060, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
+ 0x0068, 0x0069, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F,
+ 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077,
+ 0x0078, 0x0079, 0x007A, 0x007B, 0x007C, 0x007D, 0x007E, 0x0020
+ };
+
+ /**
+ * DUTCH.
+ */
+ public static final char [] NL = {
+ 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007,
+ 0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
+ 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017,
+ 0x0018, 0x0019, 0x001A, 0x001B, 0x001C, 0x001D, 0x001E, 0x001F,
+ 0x0020, 0x0021, 0x0022, 0x00A3, 0x0024, 0x0025, 0x0026, 0x0027,
+ 0x0028, 0x0029, 0x002A, 0x002B, 0x002C, 0x002D, 0x002E, 0x002F,
+ 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
+ 0x0038, 0x0039, 0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F,
+ 0x00BE, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
+ 0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F,
+ 0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057,
+ 0x0058, 0x0059, 0x005A, 0x0133, 0x00BD, 0x007C, 0x005E, 0x005F,
+ 0x0060, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
+ 0x0068, 0x0069, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F,
+ 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077,
+ 0x0078, 0x0079, 0x007A, 0x00A8, 0x0066, 0x00BC, 0x00B4, 0x0020
+ };
+
+ /**
+ * FINNISH.
+ */
+ public static final char [] FI = {
+ 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007,
+ 0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
+ 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017,
+ 0x0018, 0x0019, 0x001A, 0x001B, 0x001C, 0x001D, 0x001E, 0x001F,
+ 0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027,
+ 0x0028, 0x0029, 0x002A, 0x002B, 0x002C, 0x002D, 0x002E, 0x002F,
+ 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
+ 0x0038, 0x0039, 0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F,
+ 0x0040, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
+ 0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F,
+ 0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057,
+ 0x0058, 0x0059, 0x005A, 0x00C4, 0x00D6, 0x00C5, 0x00DC, 0x005F,
+ 0x00E9, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
+ 0x0068, 0x0069, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F,
+ 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077,
+ 0x0078, 0x0079, 0x007A, 0x00E4, 0x00F6, 0x00E5, 0x00FC, 0x0020
+ };
+
+ /**
+ * FRENCH.
+ */
+ public static final char [] FR = {
+ 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007,
+ 0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
+ 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017,
+ 0x0018, 0x0019, 0x001A, 0x001B, 0x001C, 0x001D, 0x001E, 0x001F,
+ 0x0020, 0x0021, 0x0022, 0x00A3, 0x0024, 0x0025, 0x0026, 0x0027,
+ 0x0028, 0x0029, 0x002A, 0x002B, 0x002C, 0x002D, 0x002E, 0x002F,
+ 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
+ 0x0038, 0x0039, 0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F,
+ 0x00E0, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
+ 0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F,
+ 0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057,
+ 0x0058, 0x0059, 0x005A, 0x00B0, 0x00E7, 0x00A7, 0x005E, 0x005F,
+ 0x0060, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
+ 0x0068, 0x0069, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F,
+ 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077,
+ 0x0078, 0x0079, 0x007A, 0x00E9, 0x00F9, 0x00E8, 0x00A8, 0x0020
+ };
+
+ /**
+ * FRENCH_CA.
+ */
+ public static final char [] FR_CA = {
+ 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007,
+ 0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
+ 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017,
+ 0x0018, 0x0019, 0x001A, 0x001B, 0x001C, 0x001D, 0x001E, 0x001F,
+ 0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027,
+ 0x0028, 0x0029, 0x002A, 0x002B, 0x002C, 0x002D, 0x002E, 0x002F,
+ 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
+ 0x0038, 0x0039, 0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F,
+ 0x00E0, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
+ 0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F,
+ 0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057,
+ 0x0058, 0x0059, 0x005A, 0x00E2, 0x00E7, 0x00EA, 0x00EE, 0x005F,
+ 0x00F4, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
+ 0x0068, 0x0069, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F,
+ 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077,
+ 0x0078, 0x0079, 0x007A, 0x00E9, 0x00F9, 0x00E8, 0x00FB, 0x0020
+ };
+
+ /**
+ * GERMAN.
+ */
+ public static final char [] DE = {
+ 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007,
+ 0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
+ 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017,
+ 0x0018, 0x0019, 0x001A, 0x001B, 0x001C, 0x001D, 0x001E, 0x001F,
+ 0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027,
+ 0x0028, 0x0029, 0x002A, 0x002B, 0x002C, 0x002D, 0x002E, 0x002F,
+ 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
+ 0x0038, 0x0039, 0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F,
+ 0x00A7, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
+ 0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F,
+ 0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057,
+ 0x0058, 0x0059, 0x005A, 0x00C4, 0x00D6, 0x00DC, 0x005E, 0x005F,
+ 0x0060, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
+ 0x0068, 0x0069, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F,
+ 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077,
+ 0x0078, 0x0079, 0x007A, 0x00E4, 0x00F6, 0x00FC, 0x00DF, 0x0020
+ };
+
+ /**
+ * ITALIAN.
+ */
+ public static final char [] IT = {
+ 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007,
+ 0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
+ 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017,
+ 0x0018, 0x0019, 0x001A, 0x001B, 0x001C, 0x001D, 0x001E, 0x001F,
+ 0x0020, 0x0021, 0x0022, 0x00A3, 0x0024, 0x0025, 0x0026, 0x0027,
+ 0x0028, 0x0029, 0x002A, 0x002B, 0x002C, 0x002D, 0x002E, 0x002F,
+ 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
+ 0x0038, 0x0039, 0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F,
+ 0x00A7, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
+ 0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F,
+ 0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057,
+ 0x0058, 0x0059, 0x005A, 0x00B0, 0x00E7, 0x00E9, 0x005E, 0x005F,
+ 0x00F9, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
+ 0x0068, 0x0069, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F,
+ 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077,
+ 0x0078, 0x0079, 0x007A, 0x00E0, 0x00F2, 0x00E8, 0x00EC, 0x0020
+ };
+
+ /**
+ * NORWEGIAN.
+ */
+ public static final char [] NO = {
+ 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007,
+ 0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
+ 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017,
+ 0x0018, 0x0019, 0x001A, 0x001B, 0x001C, 0x001D, 0x001E, 0x001F,
+ 0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027,
+ 0x0028, 0x0029, 0x002A, 0x002B, 0x002C, 0x002D, 0x002E, 0x002F,
+ 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
+ 0x0038, 0x0039, 0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F,
+ 0x00C4, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
+ 0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F,
+ 0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057,
+ 0x0058, 0x0059, 0x005A, 0x00C6, 0x00D8, 0x00C5, 0x00DC, 0x005F,
+ 0x00E4, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
+ 0x0068, 0x0069, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F,
+ 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077,
+ 0x0078, 0x0079, 0x007A, 0x00E6, 0x00F8, 0x00E5, 0x00FC, 0x0020
+ };
+
+ /**
+ * SPANISH.
+ */
+ public static final char [] ES = {
+ 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007,
+ 0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
+ 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017,
+ 0x0018, 0x0019, 0x001A, 0x001B, 0x001C, 0x001D, 0x001E, 0x001F,
+ 0x0020, 0x0021, 0x0022, 0x00A3, 0x0024, 0x0025, 0x0026, 0x0027,
+ 0x0028, 0x0029, 0x002A, 0x002B, 0x002C, 0x002D, 0x002E, 0x002F,
+ 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
+ 0x0038, 0x0039, 0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F,
+ 0x00A7, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
+ 0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F,
+ 0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057,
+ 0x0058, 0x0059, 0x005A, 0x00A1, 0x00D1, 0x00BF, 0x005E, 0x005F,
+ 0x0060, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
+ 0x0068, 0x0069, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F,
+ 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077,
+ 0x0078, 0x0079, 0x007A, 0x00B0, 0x00F1, 0x00E7, 0x007E, 0x0020
+ };
+
+ /**
+ * SWEDISH.
+ */
+ public static final char [] SV = {
+ 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007,
+ 0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
+ 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017,
+ 0x0018, 0x0019, 0x001A, 0x001B, 0x001C, 0x001D, 0x001E, 0x001F,
+ 0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027,
+ 0x0028, 0x0029, 0x002A, 0x002B, 0x002C, 0x002D, 0x002E, 0x002F,
+ 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
+ 0x0038, 0x0039, 0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F,
+ 0x00C9, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
+ 0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F,
+ 0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057,
+ 0x0058, 0x0059, 0x005A, 0x00C4, 0x00D6, 0x00C5, 0x00DC, 0x005F,
+ 0x00E9, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
+ 0x0068, 0x0069, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F,
+ 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077,
+ 0x0078, 0x0079, 0x007A, 0x00E4, 0x00F6, 0x00E5, 0x00FC, 0x0020
+ };
+
+ /**
+ * SWISS.
+ */
+ public static final char [] SWISS = {
+ 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007,
+ 0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
+ 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017,
+ 0x0018, 0x0019, 0x001A, 0x001B, 0x001C, 0x001D, 0x001E, 0x001F,
+ 0x0020, 0x0021, 0x0022, 0x00F9, 0x0024, 0x0025, 0x0026, 0x0027,
+ 0x0028, 0x0029, 0x002A, 0x002B, 0x002C, 0x002D, 0x002E, 0x002F,
+ 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
+ 0x0038, 0x0039, 0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F,
+ 0x00E0, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
+ 0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F,
+ 0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057,
+ 0x0058, 0x0059, 0x005A, 0x00E9, 0x00E7, 0x00EA, 0x00EE, 0x00E8,
+ 0x00F4, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
+ 0x0068, 0x0069, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F,
+ 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077,
+ 0x0078, 0x0079, 0x007A, 0x00E4, 0x00F6, 0x00FC, 0x00FB, 0x0020
+ };
+
+ /**
+ * VT52 drawing characters.
+ */
+ public static final char [] VT52_SPECIAL_GRAPHICS = {
+ 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007,
+ 0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
+ 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017,
+ 0x0018, 0x0019, 0x001A, 0x001B, 0x001C, 0x001D, 0x001E, 0x001F,
+ 0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027,
+ 0x0028, 0x0029, 0x002A, 0x002B, 0x002C, 0x002D, 0x002E, 0x002F,
+ 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
+ 0x0038, 0x0039, 0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F,
+ 0x0040, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
+ 0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F,
+ 0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057,
+ 0x0058, 0x0059, 0x005A, 0x005B, 0x005C, 0x005D, 0x0020, 0x0020,
+ 0x0020, 0x2588, 0x215F, 0x2592, 0x2592, 0x2592, 0x00B0, 0x00B1,
+ 0x2190, 0x2026, 0x00F7, 0x2193, 0x23BA, 0x23BA, 0x23BB, 0x23BB,
+ 0x2500, 0x2500, 0x23BC, 0x23BC, 0x2080, 0x2081, 0x2082, 0x2083,
+ 0x2084, 0x2085, 0x2086, 0x2087, 0x2088, 0x2089, 0x00B6, 0x0020
+ };
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Private constructor prevents accidental creation of this class.
+ */
+ private DECCharacterSets() {
+ }
+
+ }
--- /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.tterminal;
+
+ import jexer.bits.Cell;
+ import jexer.bits.CellAttributes;
+
+ /**
+ * This represents a single line of the display buffer.
+ */
+ public class DisplayLine {
+
+ // ------------------------------------------------------------------------
+ // Constants --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Maximum line length.
+ */
+ private static final int MAX_LINE_LENGTH = 256;
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The characters/attributes of the line.
+ */
+ private Cell [] chars;
+
+ /**
+ * Double-width line flag.
+ */
+ private boolean doubleWidth = false;
+
+ /**
+ * Double height line flag. Valid values are:
+ *
+ * <p><pre>
+ * 0 = single height
+ * 1 = top half double height
+ * 2 = bottom half double height
+ * </pre>
+ */
+ private int doubleHeight = 0;
+
+ /**
+ * DECSCNM - reverse video. We copy the flag to the line so that
+ * reverse-mode scrollback lines still show inverted colors correctly.
+ */
+ private boolean reverseColor = false;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor makes a duplicate (deep copy).
+ *
+ * @param line the line to duplicate
+ */
+ public DisplayLine(final DisplayLine line) {
+ chars = new Cell[MAX_LINE_LENGTH];
+ for (int i = 0; i < chars.length; i++) {
+ chars[i] = new Cell(line.chars[i]);
+ }
+ doubleWidth = line.doubleWidth;
+ doubleHeight = line.doubleHeight;
+ reverseColor = line.reverseColor;
+ }
+
+ /**
+ * Public constructor sets everything to drawing attributes.
+ *
+ * @param attr current drawing attributes
+ */
+ public DisplayLine(final CellAttributes attr) {
+ chars = new Cell[MAX_LINE_LENGTH];
+ for (int i = 0; i < chars.length; i++) {
+ chars[i] = new Cell(attr);
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // DisplayLine ------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get the Cell at a specific column.
+ *
+ * @param idx the character index
+ * @return the Cell
+ */
+ public Cell charAt(final int idx) {
+ return chars[idx];
+ }
+
+ /**
+ * Get the length of this line.
+ *
+ * @return line length
+ */
+ public int length() {
+ return chars.length;
+ }
+
+ /**
+ * Get double width flag.
+ *
+ * @return double width
+ */
+ public boolean isDoubleWidth() {
+ return doubleWidth;
+ }
+
+ /**
+ * Set double width flag.
+ *
+ * @param doubleWidth new value for double width flag
+ */
+ public void setDoubleWidth(final boolean doubleWidth) {
+ this.doubleWidth = doubleWidth;
+ }
+
+ /**
+ * Get double height flag.
+ *
+ * @return double height
+ */
+ public int getDoubleHeight() {
+ return doubleHeight;
+ }
+
+ /**
+ * Set double height flag.
+ *
+ * @param doubleHeight new value for double height flag
+ */
+ public void setDoubleHeight(final int doubleHeight) {
+ this.doubleHeight = doubleHeight;
+ }
+
+ /**
+ * Get reverse video flag.
+ *
+ * @return reverse video
+ */
+ public boolean isReverseColor() {
+ return reverseColor;
+ }
+
+ /**
+ * Set double-height flag.
+ *
+ * @param reverseColor new value for reverse video flag
+ */
+ public void setReverseColor(final boolean reverseColor) {
+ this.reverseColor = reverseColor;
+ }
+
+ /**
+ * Insert a character at the specified position.
+ *
+ * @param idx the character index
+ * @param newCell the new Cell
+ */
+ public void insert(final int idx, final Cell newCell) {
+ System.arraycopy(chars, idx, chars, idx + 1, chars.length - idx - 1);
+ chars[idx] = new Cell(newCell);
+ }
+
+ /**
+ * Replace character at the specified position.
+ *
+ * @param idx the character index
+ * @param newCell the new Cell
+ */
+ public void replace(final int idx, final Cell newCell) {
+ chars[idx].setTo(newCell);
+ }
+
+ /**
+ * Set the Cell at the specified position to the blank (reset).
+ *
+ * @param idx the character index
+ */
+ public void setBlank(final int idx) {
+ chars[idx].reset();
+ }
+
+ /**
+ * Set the character (just the char, not the attributes) at the specified
+ * position to ch.
+ *
+ * @param idx the character index
+ * @param ch the new char
+ */
+ public void setChar(final int idx, final int ch) {
+ chars[idx].setChar(ch);
+ }
+
+ /**
+ * Set the attributes (just the attributes, not the char) at the
+ * specified position to attr.
+ *
+ * @param idx the character index
+ * @param attr the new attributes
+ */
+ public void setAttr(final int idx, final CellAttributes attr) {
+ chars[idx].setAttr(attr);
+ }
+
+ /**
+ * Delete character at the specified position, filling in the new
+ * character on the right with newCell.
+ *
+ * @param idx the character index
+ * @param newCell the new Cell
+ */
+ public void delete(final int idx, final Cell newCell) {
+ System.arraycopy(chars, idx + 1, chars, idx, chars.length - idx - 1);
+ chars[chars.length - 1] = new Cell(newCell);
+ }
+
+ }
--- /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.tterminal;
+
+ /**
+ * DisplayListener is used to callback into external UI when data has come in
+ * from the remote side.
+ */
+ public interface DisplayListener {
+
+ /**
+ * Function to call when the display needs to be updated.
+ */
+ public void displayChanged();
+
+ /**
+ * Function to call to obtain the display width.
+ *
+ * @return the number of columns in the display
+ */
+ public int getDisplayWidth();
+
+ /**
+ * Function to call to obtain the display height.
+ *
+ * @return the number of rows in the display
+ */
+ public int getDisplayHeight();
+
+ }
--- /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.tterminal;
+
+ import java.awt.Graphics2D;
+ import java.awt.image.BufferedImage;
+ import java.io.BufferedOutputStream;
+ import java.io.CharArrayWriter;
+ import java.io.InputStream;
+ import java.io.InputStreamReader;
+ import java.io.IOException;
+ import java.io.OutputStream;
+ import java.io.OutputStreamWriter;
+ import java.io.PrintWriter;
+ import java.io.Reader;
+ import java.io.UnsupportedEncodingException;
+ import java.io.Writer;
+ import java.util.ArrayList;
+ import java.util.Collections;
+ import java.util.HashMap;
+ import java.util.List;
+
+ import jexer.TKeypress;
+ import jexer.backend.GlyphMaker;
+ import jexer.bits.Color;
+ import jexer.bits.Cell;
+ import jexer.bits.CellAttributes;
+ import jexer.bits.StringUtils;
+ import jexer.event.TInputEvent;
+ import jexer.event.TKeypressEvent;
+ import jexer.event.TMouseEvent;
+ import jexer.io.ReadTimeoutException;
+ import jexer.io.TimeoutInputStream;
+ import static jexer.TKeypress.*;
+
+ /**
+ * This implements a complex ECMA-48/ISO 6429/ANSI X3.64 type console,
+ * including a scrollback buffer.
+ *
+ * <p>
+ * It currently implements VT100, VT102, VT220, and XTERM with the following
+ * caveats:
+ *
+ * <p>
+ * - The vttest scenario for VT220 8-bit controls (11.1.2.3) reports a
+ * failure with XTERM. This is due to vttest failing to decode the UTF-8
+ * stream.
+ *
+ * <p>
+ * - Smooth scrolling, printing, keyboard locking, keyboard leds, and tests
+ * from VT100 are not supported.
+ *
+ * <p>
+ * - User-defined keys (DECUDK), downloadable fonts (DECDLD), and VT100/ANSI
+ * compatibility mode (DECSCL) from VT220 are not supported. (Also,
+ * because DECSCL is not supported, it will fail the last part of the
+ * vttest "Test of VT52 mode" if DeviceType is set to VT220.)
+ *
+ * <p>
+ * - Numeric/application keys from the number pad are not supported because
+ * they are not exposed from the TKeypress API.
+ *
+ * <p>
+ * - VT52 HOLD SCREEN mode is not supported.
+ *
+ * <p>
+ * - In VT52 graphics mode, the 3/, 5/, and 7/ characters (fraction
+ * numerators) are not rendered correctly.
+ *
+ * <p>
+ * - All data meant for the 'printer' (CSI Pc ? i) is discarded.
+ */
+ public class ECMA48 implements Runnable {
+
+ // ------------------------------------------------------------------------
+ // Constants --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The emulator can emulate several kinds of terminals.
+ */
+ public enum DeviceType {
+ /**
+ * DEC VT100 but also including the three VT102 functions.
+ */
+ VT100,
+
+ /**
+ * DEC VT102.
+ */
+ VT102,
+
+ /**
+ * DEC VT220.
+ */
+ VT220,
+
+ /**
+ * A subset of xterm.
+ */
+ XTERM
+ }
+
+ /**
+ * Parser character scan states.
+ */
+ private enum ScanState {
+ GROUND,
+ ESCAPE,
+ ESCAPE_INTERMEDIATE,
+ CSI_ENTRY,
+ CSI_PARAM,
+ CSI_INTERMEDIATE,
+ CSI_IGNORE,
+ DCS_ENTRY,
+ DCS_INTERMEDIATE,
+ DCS_PARAM,
+ DCS_PASSTHROUGH,
+ DCS_IGNORE,
+ DCS_SIXEL,
+ SOSPMAPC_STRING,
+ OSC_STRING,
+ VT52_DIRECT_CURSOR_ADDRESS
+ }
+
+ /**
+ * The selected number pad mode (DECKPAM, DECKPNM). We record this, but
+ * can't really use it in keypress() because we do not see number pad
+ * events from TKeypress.
+ */
+ private enum KeypadMode {
+ Application,
+ Numeric
+ }
+
+ /**
+ * Arrow keys can emit three different sequences (DECCKM or VT52
+ * submode).
+ */
+ private enum ArrowKeyMode {
+ VT52,
+ ANSI,
+ VT100
+ }
+
+ /**
+ * Available character sets for GL, GR, G0, G1, G2, G3.
+ */
+ private enum CharacterSet {
+ US,
+ UK,
+ DRAWING,
+ ROM,
+ ROM_SPECIAL,
+ VT52_GRAPHICS,
+ DEC_SUPPLEMENTAL,
+ NRC_DUTCH,
+ NRC_FINNISH,
+ NRC_FRENCH,
+ NRC_FRENCH_CA,
+ NRC_GERMAN,
+ NRC_ITALIAN,
+ NRC_NORWEGIAN,
+ NRC_SPANISH,
+ NRC_SWEDISH,
+ NRC_SWISS
+ }
+
+ /**
+ * Single-shift states used by the C1 control characters SS2 (0x8E) and
+ * SS3 (0x8F).
+ */
+ private enum Singleshift {
+ NONE,
+ SS2,
+ SS3
+ }
+
+ /**
+ * VT220+ lockshift states.
+ */
+ private enum LockshiftMode {
+ NONE,
+ G1_GR,
+ G2_GR,
+ G2_GL,
+ G3_GR,
+ G3_GL
+ }
+
+ /**
+ * XTERM mouse reporting protocols.
+ */
+ public enum MouseProtocol {
+ OFF,
+ X10,
+ NORMAL,
+ BUTTONEVENT,
+ ANYEVENT
+ }
+
+ /**
+ * XTERM mouse reporting encodings.
+ */
+ private enum MouseEncoding {
+ X10,
+ UTF8,
+ SGR
+ }
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The enclosing listening object.
+ */
+ private DisplayListener displayListener;
+
+ /**
+ * When true, the reader thread is expected to exit.
+ */
+ private volatile boolean stopReaderThread = false;
+
+ /**
+ * The reader thread.
+ */
+ private Thread readerThread = null;
+
+ /**
+ * The type of emulator to be.
+ */
+ private DeviceType type = DeviceType.VT102;
+
+ /**
+ * The scrollback buffer characters + attributes.
+ */
+ private volatile ArrayList<DisplayLine> scrollback;
+
+ /**
+ * The raw display buffer characters + attributes.
+ */
+ private volatile ArrayList<DisplayLine> display;
+
+ /**
+ * The maximum number of lines in the scrollback buffer.
+ */
+ private int maxScrollback = 10000;
+
+ /**
+ * The terminal's input. For type == XTERM, this is an InputStreamReader
+ * with UTF-8 encoding.
+ */
+ private Reader input;
+
+ /**
+ * The terminal's raw InputStream. This is used for type != XTERM.
+ */
+ private volatile TimeoutInputStream inputStream;
+
+ /**
+ * The terminal's output. For type == XTERM, this wraps an
+ * OutputStreamWriter with UTF-8 encoding.
+ */
+ private Writer output;
+
+ /**
+ * The terminal's raw OutputStream. This is used for type != XTERM.
+ */
+ private OutputStream outputStream;
+
+ /**
+ * Current scanning state.
+ */
+ private ScanState scanState;
+
+ /**
+ * Which mouse protocol is active.
+ */
+ private MouseProtocol mouseProtocol = MouseProtocol.OFF;
+
+ /**
+ * Which mouse encoding is active.
+ */
+ private MouseEncoding mouseEncoding = MouseEncoding.X10;
+
+ /**
+ * A terminal may request that the mouse pointer be hidden using a
+ * Privacy Message containing either "hideMousePointer" or
+ * "showMousePointer". This is currently only used within Jexer by
+ * TTerminalWindow so that only the bottom-most instance of nested
+ * Jexer's draws the mouse within its application window.
+ */
+ private boolean hideMousePointer = false;
+
+ /**
+ * Physical display width. We start at 80x24, but the user can resize us
+ * bigger/smaller.
+ */
+ private int width;
+
+ /**
+ * Physical display height. We start at 80x24, but the user can resize
+ * us bigger/smaller.
+ */
+ private int height;
+
+ /**
+ * Top margin of the scrolling region.
+ */
+ private int scrollRegionTop;
+
+ /**
+ * Bottom margin of the scrolling region.
+ */
+ private int scrollRegionBottom;
+
+ /**
+ * 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;
+
+ /**
+ * Last character printed.
+ */
+ private int repCh;
+
+ /**
+ * VT100-style line wrapping: a character is placed in column 80 (or
+ * 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;
+
+ /**
+ * VT220 single shift flag.
+ */
+ private Singleshift singleshift = Singleshift.NONE;
+
+ /**
+ * true = insert characters, false = overwrite.
+ */
+ private boolean insertMode = false;
+
+ /**
+ * VT52 mode as selected by DECANM. True means VT52, false means
+ * ANSI. Default is ANSI.
+ */
+ private boolean vt52Mode = false;
+
+ /**
+ * Visible cursor (DECTCEM).
+ */
+ private boolean cursorVisible = true;
+
+ /**
+ * Screen title as set by the xterm OSC sequence. Lots of applications
+ * send a screenTitle regardless of whether it is an xterm client or not.
+ */
+ private String screenTitle = "";
+
+ /**
+ * Parameter characters being collected.
+ */
+ private List<Integer> csiParams;
+
+ /**
+ * Non-csi collect buffer.
+ */
+ private StringBuilder collectBuffer;
+
+ /**
+ * When true, use the G1 character set.
+ */
+ private boolean shiftOut = false;
+
+ /**
+ * Horizontal tab stop locations.
+ */
+ private List<Integer> tabStops;
+
+ /**
+ * S8C1T. True means 8bit controls, false means 7bit controls.
+ */
+ private boolean s8c1t = false;
+
+ /**
+ * Printer mode. True means send all output to printer, which discards
+ * it.
+ */
+ private boolean printerControllerMode = false;
+
+ /**
+ * LMN line mode. If true, linefeed() puts the cursor on the first
+ * column of the next line. If false, linefeed() puts the cursor one
+ * line down on the current line. The default is false.
+ */
+ private boolean newLineMode = false;
+
+ /**
+ * Whether arrow keys send ANSI, VT100, or VT52 sequences.
+ */
+ private ArrowKeyMode arrowKeyMode;
+
+ /**
+ * Whether number pad keys send VT100 or VT52, application or numeric
+ * sequences.
+ */
+ @SuppressWarnings("unused")
+ private KeypadMode keypadMode;
+
+ /**
+ * When true, the terminal is in 132-column mode (DECCOLM).
+ */
+ private boolean columns132 = false;
+
+ /**
+ * true = reverse video. Set by DECSCNM.
+ */
+ private boolean reverseVideo = false;
+
+ /**
+ * false = echo characters locally.
+ */
+ private boolean fullDuplex = true;
+
+ /**
+ * The current terminal state.
+ */
+ private SaveableState currentState;
+
+ /**
+ * The last saved terminal state.
+ */
+ private SaveableState savedState;
+
+ /**
+ * The 88- or 256-color support RGB colors.
+ */
+ private List<Integer> colors88;
+
+ /**
+ * Sixel collection buffer.
+ */
+ private StringBuilder sixelParseBuffer;
+
+ /**
+ * Sixel shared palette.
+ */
+ private HashMap<Integer, java.awt.Color> sixelPalette;
+
+ /**
+ * The width of a character cell in pixels.
+ */
+ private int textWidth = 16;
+
+ /**
+ * The height of a character cell in pixels.
+ */
+ private int textHeight = 20;
+
+ /**
+ * The last used height of a character cell in pixels, only used for
+ * full-width chars.
+ */
+ private int lastTextHeight = -1;
+
+ /**
+ * The glyph drawer for full-width chars.
+ */
+ private GlyphMaker glyphMaker = null;
+
+ /**
+ * Input queue for keystrokes and mouse events to send to the remote
+ * side.
+ */
+ private ArrayList<TInputEvent> userQueue = new ArrayList<TInputEvent>();
+
+ /**
+ * DECSC/DECRC save/restore a subset of the total state. This class
+ * encapsulates those specific flags/modes.
+ */
+ private class SaveableState {
+
+ /**
+ * When true, cursor positions are relative to the scrolling region.
+ */
+ public boolean originMode = false;
+
+ /**
+ * The current editing X position.
+ */
+ public int cursorX = 0;
+
+ /**
+ * The current editing Y position.
+ */
+ public int cursorY = 0;
+
+ /**
+ * Which character set is currently selected in G0.
+ */
+ public CharacterSet g0Charset = CharacterSet.US;
+
+ /**
+ * Which character set is currently selected in G1.
+ */
+ public CharacterSet g1Charset = CharacterSet.DRAWING;
+
+ /**
+ * Which character set is currently selected in G2.
+ */
+ public CharacterSet g2Charset = CharacterSet.US;
+
+ /**
+ * Which character set is currently selected in G3.
+ */
+ public CharacterSet g3Charset = CharacterSet.US;
+
+ /**
+ * Which character set is currently selected in GR.
+ */
+ public CharacterSet grCharset = CharacterSet.DRAWING;
+
+ /**
+ * The current drawing attributes.
+ */
+ public CellAttributes attr;
+
+ /**
+ * GL lockshift mode.
+ */
+ public LockshiftMode glLockshift = LockshiftMode.NONE;
+
+ /**
+ * GR lockshift mode.
+ */
+ public LockshiftMode grLockshift = LockshiftMode.NONE;
+
+ /**
+ * Line wrap.
+ */
+ public boolean lineWrap = true;
+
+ /**
+ * Reset to defaults.
+ */
+ public void reset() {
+ originMode = false;
+ cursorX = 0;
+ cursorY = 0;
+ g0Charset = CharacterSet.US;
+ g1Charset = CharacterSet.DRAWING;
+ g2Charset = CharacterSet.US;
+ g3Charset = CharacterSet.US;
+ grCharset = CharacterSet.DRAWING;
+ attr = new CellAttributes();
+ glLockshift = LockshiftMode.NONE;
+ grLockshift = LockshiftMode.NONE;
+ lineWrap = true;
+ }
+
+ /**
+ * Copy attributes from another instance.
+ *
+ * @param that the other instance to match
+ */
+ public void setTo(final SaveableState that) {
+ this.originMode = that.originMode;
+ this.cursorX = that.cursorX;
+ this.cursorY = that.cursorY;
+ this.g0Charset = that.g0Charset;
+ this.g1Charset = that.g1Charset;
+ this.g2Charset = that.g2Charset;
+ this.g3Charset = that.g3Charset;
+ this.grCharset = that.grCharset;
+ this.attr = new CellAttributes();
+ this.attr.setTo(that.attr);
+ this.glLockshift = that.glLockshift;
+ this.grLockshift = that.grLockshift;
+ this.lineWrap = that.lineWrap;
+ }
+
+ /**
+ * Public constructor.
+ */
+ public SaveableState() {
+ reset();
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param type one of the DeviceType constants to select VT100, VT102,
+ * VT220, or XTERM
+ * @param inputStream an InputStream connected to the remote side. For
+ * type == XTERM, inputStream is converted to a Reader with UTF-8
+ * encoding.
+ * @param outputStream an OutputStream connected to the remote user. For
+ * type == XTERM, outputStream is converted to a Writer with UTF-8
+ * encoding.
+ * @param displayListener a callback to the outer display, or null for
+ * default VT100 behavior
+ * @throws UnsupportedEncodingException if an exception is thrown when
+ * creating the InputStreamReader
+ */
+ public ECMA48(final DeviceType type, final InputStream inputStream,
+ final OutputStream outputStream, final DisplayListener displayListener)
+ throws UnsupportedEncodingException {
+
+ assert (inputStream != null);
+ assert (outputStream != null);
+
+ csiParams = new ArrayList<Integer>();
+ tabStops = new ArrayList<Integer>();
+ scrollback = new ArrayList<DisplayLine>();
+ display = new ArrayList<DisplayLine>();
+
+ this.type = type;
+ if (inputStream instanceof TimeoutInputStream) {
+ this.inputStream = (TimeoutInputStream)inputStream;
+ } else {
+ this.inputStream = new TimeoutInputStream(inputStream, 2000);
+ }
+ if (type == DeviceType.XTERM) {
+ this.input = new InputStreamReader(this.inputStream, "UTF-8");
+ this.output = new OutputStreamWriter(new
+ BufferedOutputStream(outputStream), "UTF-8");
+ this.outputStream = null;
+ } else {
+ this.output = null;
+ this.outputStream = new BufferedOutputStream(outputStream);
+ }
+ this.displayListener = displayListener;
+
+ reset();
+ for (int i = 0; i < height; i++) {
+ display.add(new DisplayLine(currentState.attr));
+ }
+
+ // Spin up the input reader
+ readerThread = new Thread(this);
+ readerThread.start();
+ }
+
+ // ------------------------------------------------------------------------
+ // Runnable ---------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Read function runs on a separate thread.
+ */
+ public final void run() {
+ boolean utf8 = false;
+ boolean done = false;
+
+ if (type == DeviceType.XTERM) {
+ utf8 = true;
+ }
+
+ // available() will often return > 1, so we need to read in chunks to
+ // stay caught up.
+ char [] readBufferUTF8 = null;
+ byte [] readBuffer = null;
+ if (utf8) {
+ readBufferUTF8 = new char[2048];
+ } else {
+ readBuffer = new byte[2048];
+ }
+
+ while (!done && !stopReaderThread) {
+ synchronized (userQueue) {
+ while (userQueue.size() > 0) {
+ handleUserEvent(userQueue.remove(0));
+ }
+ }
+
+ try {
+ int n = inputStream.available();
+
+ // System.err.printf("available() %d\n", n); System.err.flush();
+ if (utf8) {
+ if (readBufferUTF8.length < n) {
+ // The buffer wasn't big enough, make it huger
+ int newSizeHalf = Math.max(readBufferUTF8.length,
+ n);
+
+ readBufferUTF8 = new char[newSizeHalf * 2];
+ }
+ } else {
+ if (readBuffer.length < n) {
+ // The buffer wasn't big enough, make it huger
+ int newSizeHalf = Math.max(readBuffer.length, n);
+ readBuffer = new byte[newSizeHalf * 2];
+ }
+ }
+ if (n == 0) {
+ try {
+ Thread.sleep(10);
+ } catch (InterruptedException e) {
+ // SQUASH
+ }
+ continue;
+ }
+
+ int rc = -1;
+ try {
+ if (utf8) {
+ rc = input.read(readBufferUTF8, 0,
+ readBufferUTF8.length);
+ } else {
+ rc = inputStream.read(readBuffer, 0,
+ readBuffer.length);
+ }
+ } catch (ReadTimeoutException e) {
+ rc = 0;
+ }
+
+ // System.err.printf("read() %d\n", rc); System.err.flush();
+ if (rc == -1) {
+ // This is EOF
+ done = true;
+ } else {
+ // Don't step on UI events
+ synchronized (this) {
+ if (utf8) {
+ for (int i = 0; i < rc;) {
+ int ch = Character.codePointAt(readBufferUTF8,
+ i);
+ i += Character.charCount(ch);
+ consume(ch);
+ }
+ } else {
+ for (int i = 0; i < rc; i++) {
+ consume(readBuffer[i]);
+ }
+ }
+ }
+ // Permit my enclosing UI to know that I updated.
+ if (displayListener != null) {
+ displayListener.displayChanged();
+ }
+ }
+ // System.err.println("end while loop"); System.err.flush();
+ } catch (IOException e) {
+ done = true;
+
+ // This is an unusual case. We want to see the stack trace,
+ // but it is related to the spawned process rather than the
+ // actual UI. We will generate the stack trace, and consume
+ // it as though it was emitted by the shell.
+ CharArrayWriter writer= new CharArrayWriter();
+ // Send a ST and RIS to clear the emulator state.
+ try {
+ writer.write("\033\\\033c");
+ writer.write("\n-----------------------------------\n");
+ e.printStackTrace(new PrintWriter(writer));
+ writer.write("\n-----------------------------------\n");
+ } catch (IOException e2) {
+ // SQUASH
+ }
+ char [] stackTrace = writer.toCharArray();
+ for (int i = 0; i < stackTrace.length; i++) {
+ if (stackTrace[i] == '\n') {
+ consume('\r');
+ }
+ consume(stackTrace[i]);
+ }
+ }
+
+ } // while ((done == false) && (stopReaderThread == false))
+
+ // Let the rest of the world know that I am done.
+ stopReaderThread = true;
+
+ try {
+ inputStream.cancelRead();
+ inputStream.close();
+ inputStream = null;
+ } catch (IOException e) {
+ // SQUASH
+ }
+ try {
+ input.close();
+ input = null;
+ } catch (IOException e) {
+ // SQUASH
+ }
+
+ // Permit my enclosing UI to know that I updated.
+ if (displayListener != null) {
+ displayListener.displayChanged();
+ }
+
+ // System.err.println("*** run() exiting..."); System.err.flush();
+ }
+
+ // ------------------------------------------------------------------------
+ // ECMA48 -----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Process keyboard and mouse events from the user.
+ *
+ * @param event the input event to consume
+ */
+ private void handleUserEvent(final TInputEvent event) {
+ if (event instanceof TKeypressEvent) {
+ keypress(((TKeypressEvent) event).getKey());
+ }
+ if (event instanceof TMouseEvent) {
+ mouse((TMouseEvent) event);
+ }
+ }
+
+ /**
+ * Add a keyboard and mouse event from the user to the queue.
+ *
+ * @param event the input event to consume
+ */
+ public void addUserEvent(final TInputEvent event) {
+ synchronized (userQueue) {
+ userQueue.add(event);
+ }
+ }
+
+ /**
+ * Return the proper primary Device Attributes string.
+ *
+ * @return string to send to remote side that is appropriate for the
+ * this.type
+ */
+ private String deviceTypeResponse() {
+ switch (type) {
+ case VT100:
+ // "I am a VT100 with advanced video option" (often VT102)
+ return "\033[?1;2c";
+
+ case VT102:
+ // "I am a VT102"
+ return "\033[?6c";
+
+ case VT220:
+ case XTERM:
+ // "I am a VT220" - 7 bit version
+ if (!s8c1t) {
+ return "\033[?62;1;6;9;4;22c";
+ // 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";
+ default:
+ throw new IllegalArgumentException("Invalid device type: " + type);
+ }
+ }
+
+ /**
+ * Return the proper TERM environment variable for this device type.
+ *
+ * @param deviceType DeviceType.VT100, DeviceType, XTERM, etc.
+ * @return "vt100", "xterm", etc.
+ */
+ public static String deviceTypeTerm(final DeviceType deviceType) {
+ switch (deviceType) {
+ case VT100:
+ return "vt100";
+
+ case VT102:
+ return "vt102";
+
+ case VT220:
+ return "vt220";
+
+ case XTERM:
+ return "xterm";
+
+ default:
+ throw new IllegalArgumentException("Invalid device type: "
+ + deviceType);
+ }
+ }
+
+ /**
+ * Return the proper LANG for this device type. Only XTERM devices know
+ * about UTF-8, the others are defined by their standard to be either
+ * 7-bit or 8-bit characters only.
+ *
+ * @param deviceType DeviceType.VT100, DeviceType, XTERM, etc.
+ * @param baseLang a base language without UTF-8 flag such as "C" or
+ * "en_US"
+ * @return "en_US", "en_US.UTF-8", etc.
+ */
+ public static String deviceTypeLang(final DeviceType deviceType,
+ final String baseLang) {
+
+ switch (deviceType) {
+
+ case VT100:
+ case VT102:
+ case VT220:
+ return baseLang;
+
+ case XTERM:
+ return baseLang + ".UTF-8";
+
+ default:
+ throw new IllegalArgumentException("Invalid device type: "
+ + deviceType);
+ }
+ }
+
+ /**
+ * Write a string directly to the remote side.
+ *
+ * @param str string to send
+ */
+ public void writeRemote(final String str) {
+ if (stopReaderThread) {
+ // Reader hit EOF, bail out now.
+ close();
+ return;
+ }
+
+ // System.err.printf("writeRemote() '%s'\n", str);
+
+ switch (type) {
+ case VT100:
+ case VT102:
+ case VT220:
+ if (outputStream == null) {
+ return;
+ }
+ try {
+ outputStream.flush();
+ for (int i = 0; i < str.length(); i++) {
+ outputStream.write(str.charAt(i));
+ }
+ outputStream.flush();
+ } catch (IOException e) {
+ // Assume EOF
+ close();
+ }
+ break;
+ case XTERM:
+ if (output == null) {
+ return;
+ }
+ try {
+ output.flush();
+ output.write(str);
+ output.flush();
+ } catch (IOException e) {
+ // Assume EOF
+ close();
+ }
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid device type: " + type);
+ }
+ }
+
+ /**
+ * Close the input and output streams and stop the reader thread. Note
+ * that it is safe to call this multiple times.
+ */
+ public final void close() {
+
+ // Tell the reader thread to stop looking at input. It will close
+ // the input streams.
+ if (stopReaderThread == false) {
+ stopReaderThread = true;
+ try {
+ readerThread.join(1000);
+ } catch (InterruptedException e) {
+ // SQUASH
+ }
+ }
+
+ // Now close the output stream.
+ switch (type) {
+ case VT100:
+ case VT102:
+ case VT220:
+ if (outputStream != null) {
+ try {
+ outputStream.close();
+ } catch (IOException e) {
+ // SQUASH
+ }
+ outputStream = null;
+ }
+ break;
+ case XTERM:
+ if (outputStream != null) {
+ try {
+ outputStream.close();
+ } catch (IOException e) {
+ // SQUASH
+ }
+ outputStream = null;
+ }
+ if (output != null) {
+ try {
+ output.close();
+ } catch (IOException e) {
+ // SQUASH
+ }
+ output = null;
+ }
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid device type: " +
+ type);
+ }
+ }
+
+ /**
+ * See if the reader thread is still running.
+ *
+ * @return if true, we are still connected to / reading from the remote
+ * side
+ */
+ public final boolean isReading() {
+ return (!stopReaderThread);
+ }
+
+ /**
+ * Obtain a new blank display line for an external user
+ * (e.g. TTerminalWindow).
+ *
+ * @return new blank line
+ */
+ public final DisplayLine getBlankDisplayLine() {
+ return new DisplayLine(currentState.attr);
+ }
+
+ /**
+ * Get the scrollback buffer.
+ *
+ * @return the scrollback buffer
+ */
+ public final List<DisplayLine> getScrollbackBuffer() {
+ return scrollback;
+ }
+
+ /**
+ * Get the display buffer.
+ *
+ * @return the display buffer
+ */
+ public final List<DisplayLine> getDisplayBuffer() {
+ return display;
+ }
+
+ /**
+ * Get the visible display + scrollback buffer, offset by a specified
+ * number of rows from the bottom.
+ *
+ * @param visibleHeight the total height of the display to show
+ * @param scrollBottom the number of rows from the bottom to scroll back
+ * @return a copy of the display + scrollback buffers
+ */
+ public final List<DisplayLine> getVisibleDisplay(final int visibleHeight,
+ final int scrollBottom) {
+
+ assert (visibleHeight >= 0);
+ assert (scrollBottom >= 0);
+
+ int visibleBottom = scrollback.size() + display.size() - scrollBottom;
+
+ List<DisplayLine> preceedingBlankLines = new ArrayList<DisplayLine>();
+ int visibleTop = visibleBottom - visibleHeight;
+ if (visibleTop < 0) {
+ for (int i = visibleTop; i < 0; i++) {
+ preceedingBlankLines.add(getBlankDisplayLine());
+ }
+ visibleTop = 0;
+ }
+ assert (visibleTop >= 0);
+
+ List<DisplayLine> displayLines = new ArrayList<DisplayLine>();
+ displayLines.addAll(scrollback);
+ displayLines.addAll(display);
+
+ List<DisplayLine> visibleLines = new ArrayList<DisplayLine>();
+ visibleLines.addAll(preceedingBlankLines);
+ visibleLines.addAll(displayLines.subList(visibleTop, visibleBottom));
+
+ // Fill in the blank lines on bottom
+ int bottomBlankLines = visibleHeight - visibleLines.size();
+ assert (bottomBlankLines >= 0);
+ for (int i = 0; i < bottomBlankLines; i++) {
+ visibleLines.add(getBlankDisplayLine());
+ }
+
+ return copyBuffer(visibleLines);
+ }
+
+ /**
+ * Copy a display buffer.
+ *
+ * @param buffer the buffer to copy
+ * @return a deep copy of the buffer's data
+ */
+ private List<DisplayLine> copyBuffer(final List<DisplayLine> buffer) {
+ ArrayList<DisplayLine> result = new ArrayList<DisplayLine>(buffer.size());
+ for (DisplayLine line: buffer) {
+ result.add(new DisplayLine(line));
+ }
+ return result;
+ }
+
+ /**
+ * Get the display width.
+ *
+ * @return the width (usually 80 or 132)
+ */
+ public final int getWidth() {
+ return width;
+ }
+
+ /**
+ * Set the display width.
+ *
+ * @param width the new width
+ */
+ public final synchronized void setWidth(final int width) {
+ this.width = width;
+ rightMargin = width - 1;
+ if (currentState.cursorX >= width) {
+ currentState.cursorX = width - 1;
+ }
+ if (savedState.cursorX >= width) {
+ savedState.cursorX = width - 1;
+ }
+ }
+
+ /**
+ * Get the display height.
+ *
+ * @return the height (usually 24)
+ */
+ public final int getHeight() {
+ return height;
+ }
+
+ /**
+ * Set the display height.
+ *
+ * @param height the new height
+ */
+ public final synchronized void setHeight(final int height) {
+ int delta = height - this.height;
+ this.height = height;
+ scrollRegionBottom += delta;
+ if (scrollRegionBottom < 0) {
+ scrollRegionBottom = height;
+ }
+ if (scrollRegionTop >= scrollRegionBottom) {
+ scrollRegionTop = 0;
+ }
+ if (currentState.cursorY >= height) {
+ currentState.cursorY = height - 1;
+ }
+ if (savedState.cursorY >= height) {
+ savedState.cursorY = height - 1;
+ }
+ while (display.size() < height) {
+ DisplayLine line = new DisplayLine(currentState.attr);
+ line.setReverseColor(reverseVideo);
+ display.add(line);
+ }
+ while (display.size() > height) {
+ scrollback.add(display.remove(0));
+ }
+ }
+
+ /**
+ * Get visible cursor flag.
+ *
+ * @return if true, the cursor is visible
+ */
+ public final boolean isCursorVisible() {
+ return cursorVisible;
+ }
+
+ /**
+ * Get the screen title as set by the xterm OSC sequence. Lots of
+ * applications send a screenTitle regardless of whether it is an xterm
+ * client or not.
+ *
+ * @return screen title
+ */
+ public final String getScreenTitle() {
+ return screenTitle;
+ }
+
+ /**
+ * Get 132 columns value.
+ *
+ * @return if true, the terminal is in 132 column mode
+ */
+ public final boolean isColumns132() {
+ return columns132;
+ }
+
+ /**
+ * Clear the CSI parameters and flags.
+ */
+ private void toGround() {
+ csiParams.clear();
+ collectBuffer = new StringBuilder(8);
+ scanState = ScanState.GROUND;
+ }
+
+ /**
+ * Reset the tab stops list.
+ */
+ private void resetTabStops() {
+ tabStops.clear();
+ for (int i = 0; (i * 8) <= rightMargin; i++) {
+ tabStops.add(Integer.valueOf(i * 8));
+ }
+ }
+
+ /**
+ * Reset the 88- or 256-colors.
+ */
+ private void resetColors() {
+ colors88 = new ArrayList<Integer>(256);
+ for (int i = 0; i < 256; i++) {
+ colors88.add(0);
+ }
+
+ // Set default system colors.
+ colors88.set(0, 0x00000000);
+ colors88.set(1, 0x00a80000);
+ colors88.set(2, 0x0000a800);
+ colors88.set(3, 0x00a85400);
+ colors88.set(4, 0x000000a8);
+ colors88.set(5, 0x00a800a8);
+ colors88.set(6, 0x0000a8a8);
+ colors88.set(7, 0x00a8a8a8);
+
+ colors88.set(8, 0x00545454);
+ colors88.set(9, 0x00fc5454);
+ colors88.set(10, 0x0054fc54);
+ colors88.set(11, 0x00fcfc54);
+ colors88.set(12, 0x005454fc);
+ colors88.set(13, 0x00fc54fc);
+ colors88.set(14, 0x0054fcfc);
+ colors88.set(15, 0x00fcfcfc);
+ }
+
+ /**
+ * Get the RGB value of one of the indexed colors.
+ *
+ * @param index the color index
+ * @return the RGB value
+ */
+ private int get88Color(final int index) {
+ // System.err.print("get88Color: " + index);
+ if ((index < 0) || (index > colors88.size())) {
+ // System.err.println(" -- UNKNOWN");
+ return 0;
+ }
+ // System.err.printf(" %08x\n", colors88.get(index));
+ return colors88.get(index);
+ }
+
+ /**
+ * Set one of the indexed colors to a color specification.
+ *
+ * @param index the color index
+ * @param spec the specification, typically something like "rgb:aa/bb/cc"
+ */
+ private void set88Color(final int index, final String spec) {
+ // System.err.println("set88Color: " + index + " '" + spec + "'");
+
+ if ((index < 0) || (index > colors88.size())) {
+ return;
+ }
+ if (spec.startsWith("rgb:")) {
+ String [] rgbTokens = spec.substring(4).split("/");
+ if (rgbTokens.length == 3) {
+ try {
+ int rgb = (Integer.parseInt(rgbTokens[0], 16) << 16);
+ rgb |= Integer.parseInt(rgbTokens[1], 16) << 8;
+ rgb |= Integer.parseInt(rgbTokens[2], 16);
+ // System.err.printf(" set to %08x\n", rgb);
+ colors88.set(index, rgb);
+ } catch (NumberFormatException e) {
+ // SQUASH
+ }
+ }
+ return;
+ }
+
+ if (spec.toLowerCase().equals("black")) {
+ colors88.set(index, 0x00000000);
+ } else if (spec.toLowerCase().equals("red")) {
+ colors88.set(index, 0x00a80000);
+ } else if (spec.toLowerCase().equals("green")) {
+ colors88.set(index, 0x0000a800);
+ } else if (spec.toLowerCase().equals("yellow")) {
+ colors88.set(index, 0x00a85400);
+ } else if (spec.toLowerCase().equals("blue")) {
+ colors88.set(index, 0x000000a8);
+ } else if (spec.toLowerCase().equals("magenta")) {
+ colors88.set(index, 0x00a800a8);
+ } else if (spec.toLowerCase().equals("cyan")) {
+ colors88.set(index, 0x0000a8a8);
+ } else if (spec.toLowerCase().equals("white")) {
+ colors88.set(index, 0x00a8a8a8);
+ }
+
+ }
+
+ /**
+ * Reset the emulation state.
+ */
+ private void reset() {
+
+ currentState = new SaveableState();
+ savedState = new SaveableState();
+ scanState = ScanState.GROUND;
+ width = 80;
+ height = 24;
+ scrollRegionTop = 0;
+ scrollRegionBottom = height - 1;
+ rightMargin = width - 1;
+ newLineMode = false;
+ arrowKeyMode = ArrowKeyMode.ANSI;
+ keypadMode = KeypadMode.Numeric;
+ wrapLineFlag = false;
+ if (displayListener != null) {
+ width = displayListener.getDisplayWidth();
+ height = displayListener.getDisplayHeight();
+ rightMargin = width - 1;
+ }
+
+ // Flags
+ shiftOut = false;
+ vt52Mode = false;
+ insertMode = false;
+ columns132 = false;
+ newLineMode = false;
+ reverseVideo = false;
+ fullDuplex = true;
+ cursorVisible = true;
+
+ // VT220
+ singleshift = Singleshift.NONE;
+ s8c1t = false;
+ printerControllerMode = false;
+
+ // XTERM
+ mouseProtocol = MouseProtocol.OFF;
+ mouseEncoding = MouseEncoding.X10;
+
+ // Tab stops
+ resetTabStops();
+
+ // Reset extra colors
+ resetColors();
+
+ // Clear CSI stuff
+ toGround();
+ }
+
+ /**
+ * 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) {
+ scrollback.remove(0);
+ scrollback.trimToSize();
+ }
+ display.remove(0);
+ display.trimToSize();
+ DisplayLine line = new DisplayLine(currentState.attr);
+ line.setReverseColor(reverseVideo);
+ display.add(line);
+ }
+
+ /**
+ * Wraps the current line.
+ */
+ private void wrapCurrentLine() {
+ if (currentState.cursorY == height - 1) {
+ newDisplayLine();
+ }
+ if (currentState.cursorY < height - 1) {
+ currentState.cursorY++;
+ }
+ currentState.cursorX = 0;
+ }
+
+ /**
+ * Handle a carriage return.
+ */
+ private void carriageReturn() {
+ currentState.cursorX = 0;
+ wrapLineFlag = false;
+ }
+
+ /**
+ * Reverse the color of the visible display.
+ */
+ private void invertDisplayColors() {
+ for (DisplayLine line: display) {
+ line.setReverseColor(!line.isReverseColor());
+ }
+ }
+
+ /**
+ * Handle a linefeed.
+ */
+ private void linefeed() {
+
+ if (currentState.cursorY < scrollRegionBottom) {
+ // Increment screen y
+ currentState.cursorY++;
+ } else {
+
+ // Screen y does not increment
+
+ /*
+ * Two cases: either we're inside a scrolling region or not. If
+ * the scrolling region bottom is the bottom of the screen, then
+ * push the top line into the buffer. Else scroll the scrolling
+ * region up.
+ */
+ if ((scrollRegionBottom == height - 1) && (scrollRegionTop == 0)) {
+
+ // We're at the bottom of the scroll region, AND the scroll
+ // region is the entire screen.
+
+ // New line
+ newDisplayLine();
+
+ } else {
+ // We're at the bottom of the scroll region, AND the scroll
+ // region is NOT the entire screen.
+ scrollingRegionScrollUp(scrollRegionTop, scrollRegionBottom, 1);
+ }
+ }
+
+ if (newLineMode) {
+ currentState.cursorX = 0;
+ }
+ wrapLineFlag = false;
+ }
+
+ /**
+ * Prints one character to the display buffer.
+ *
+ * @param ch character to display
+ */
+ private void printCharacter(final int ch) {
+ int rightMargin = this.rightMargin;
+
+ if (StringUtils.width(ch) == 2) {
+ // This is a full-width character. Save two spaces, and then
+ // draw the character as two image halves.
+ int x0 = currentState.cursorX;
+ int y0 = currentState.cursorY;
+ printCharacter(' ');
+ printCharacter(' ');
+ if ((currentState.cursorX == x0 + 2)
+ && (currentState.cursorY == y0)
+ ) {
+ // We can draw both halves of the character.
+ drawHalves(x0, y0, x0 + 1, y0, ch);
+ } else if ((currentState.cursorX == x0 + 1)
+ && (currentState.cursorY == y0)
+ ) {
+ // VT100 line wrap behavior: we should be at the right
+ // margin. We can draw both halves of the character.
+ drawHalves(x0, y0, x0 + 1, y0, ch);
+ } else {
+ // The character splits across the line. Draw the entire
+ // character on the new line, giving one more space for it.
+ x0 = currentState.cursorX - 1;
+ y0 = currentState.cursorY;
+ printCharacter(' ');
+ drawHalves(x0, y0, x0 + 1, y0, ch);
+ }
+ return;
+ }
+
+ // Check if we have double-width, and if so chop at 40/66 instead of
+ // 80/132
+ if (display.get(currentState.cursorY).isDoubleWidth()) {
+ rightMargin = ((rightMargin + 1) / 2) - 1;
+ }
+
+ // Check the unusually-complicated line wrapping conditions...
+ if (currentState.cursorX == rightMargin) {
+
+ if (currentState.lineWrap == true) {
+ /*
+ * This case happens when: the cursor was already on the
+ * right margin (either through printing or by an explicit
+ * placement command), and a character was printed.
+ *
+ * The line wraps only when a new character arrives AND the
+ * cursor is already on the right margin AND has placed a
+ * character in its cell. Easier to see than to explain.
+ */
+ if (wrapLineFlag == false) {
+ /*
+ * This block marks the case that we are in the margin
+ * and the first character has been received and printed.
+ */
+ wrapLineFlag = true;
+ } else {
+ /*
+ * This block marks the case that we are in the margin
+ * and the second character has been received and
+ * printed.
+ */
+ wrapLineFlag = false;
+ wrapCurrentLine();
+ }
+ }
+ } else if (currentState.cursorX <= rightMargin) {
+ /*
+ * This is the normal case: a character came in and was printed
+ * to the left of the right margin column.
+ */
+
+ // Turn off VT100 special-case flag
+ wrapLineFlag = false;
+ }
+
+ // "Print" the character
+ Cell newCell = new Cell(ch);
+ CellAttributes newCellAttributes = (CellAttributes) newCell;
+ newCellAttributes.setTo(currentState.attr);
+ DisplayLine line = display.get(currentState.cursorY);
+
+ if (StringUtils.width(ch) == 1) {
+ // Insert mode special case
+ if (insertMode == true) {
+ line.insert(currentState.cursorX, newCell);
+ } else {
+ // Replace an existing character
+ line.replace(currentState.cursorX, newCell);
+ }
+
+ // Increment horizontal
+ if (wrapLineFlag == false) {
+ currentState.cursorX++;
+ if (currentState.cursorX > rightMargin) {
+ currentState.cursorX--;
+ }
+ }
+ }
+ }
+
+ /**
+ * Translate the mouse event to a VT100, VT220, or XTERM sequence and
+ * send to the remote side.
+ *
+ * @param mouse mouse event received from the local user
+ */
+ private void mouse(final TMouseEvent mouse) {
+
+ /*
+ System.err.printf("mouse(): protocol %s encoding %s mouse %s\n",
+ mouseProtocol, mouseEncoding, mouse);
+ */
+
+ if (mouseEncoding == MouseEncoding.X10) {
+ // We will support X10 but only for (160,94) and smaller.
+ if ((mouse.getX() >= 160) || (mouse.getY() >= 94)) {
+ return;
+ }
+ }
+
+ switch (mouseProtocol) {
+
+ case OFF:
+ // Do nothing
+ return;
+
+ case X10:
+ // Only report button presses
+ if (mouse.getType() != TMouseEvent.Type.MOUSE_DOWN) {
+ return;
+ }
+ break;
+
+ case NORMAL:
+ // Only report button presses and releases
+ if ((mouse.getType() != TMouseEvent.Type.MOUSE_DOWN)
+ && (mouse.getType() != TMouseEvent.Type.MOUSE_UP)
+ ) {
+ return;
+ }
+ break;
+
+ case BUTTONEVENT:
+ /*
+ * Only report button presses, button releases, and motions that
+ * have a button down (i.e. drag-and-drop).
+ */
+ if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) {
+ if (!mouse.isMouse1()
+ && !mouse.isMouse2()
+ && !mouse.isMouse3()
+ && !mouse.isMouseWheelUp()
+ && !mouse.isMouseWheelDown()
+ ) {
+ return;
+ }
+ }
+ break;
+
+ case ANYEVENT:
+ // Report everything
+ break;
+ }
+
+ // Now encode the event
+ StringBuilder sb = new StringBuilder(6);
+ if (mouseEncoding == MouseEncoding.SGR) {
+ sb.append((char) 0x1B);
+ sb.append("[<");
+
+ if (mouse.isMouse1()) {
+ if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) {
+ sb.append("32;");
+ } else {
+ sb.append("0;");
+ }
+ } else if (mouse.isMouse2()) {
+ if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) {
+ sb.append("33;");
+ } else {
+ sb.append("1;");
+ }
+ } else if (mouse.isMouse3()) {
+ if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) {
+ sb.append("34;");
+ } else {
+ sb.append("2;");
+ }
+ } else if (mouse.isMouseWheelUp()) {
+ sb.append("64;");
+ } else if (mouse.isMouseWheelDown()) {
+ sb.append("65;");
+ } else {
+ // This is motion with no buttons down.
+ sb.append("35;");
+ }
+
+ sb.append(String.format("%d;%d", mouse.getX() + 1,
+ mouse.getY() + 1));
+
+ if (mouse.getType() == TMouseEvent.Type.MOUSE_UP) {
+ sb.append("m");
+ } else {
+ sb.append("M");
+ }
+
+ } else {
+ // X10 and UTF8 encodings
+ sb.append((char) 0x1B);
+ sb.append('[');
+ sb.append('M');
+ if (mouse.getType() == TMouseEvent.Type.MOUSE_UP) {
+ sb.append((char) (0x03 + 32));
+ } else if (mouse.isMouse1()) {
+ if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) {
+ sb.append((char) (0x00 + 32 + 32));
+ } else {
+ sb.append((char) (0x00 + 32));
+ }
+ } else if (mouse.isMouse2()) {
+ if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) {
+ sb.append((char) (0x01 + 32 + 32));
+ } else {
+ sb.append((char) (0x01 + 32));
+ }
+ } else if (mouse.isMouse3()) {
+ if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) {
+ sb.append((char) (0x02 + 32 + 32));
+ } else {
+ sb.append((char) (0x02 + 32));
+ }
+ } else if (mouse.isMouseWheelUp()) {
+ sb.append((char) (0x04 + 64));
+ } else if (mouse.isMouseWheelDown()) {
+ sb.append((char) (0x05 + 64));
+ } else {
+ // This is motion with no buttons down.
+ sb.append((char) (0x03 + 32));
+ }
+
+ sb.append((char) (mouse.getX() + 33));
+ sb.append((char) (mouse.getY() + 33));
+ }
+
+ // System.err.printf("Would write: \'%s\'\n", sb.toString());
+ writeRemote(sb.toString());
+ }
+
+ /**
+ * Translate the keyboard press to a VT100, VT220, or XTERM sequence and
+ * send to the remote side.
+ *
+ * @param keypress keypress received from the local user
+ */
+ private void keypress(final TKeypress keypress) {
+ writeRemote(keypressToString(keypress));
+ }
+
+ /**
+ * Build one of the complex xterm keystroke sequences, storing the result in
+ * xterm_keystroke_buffer.
+ *
+ * @param ss3 the prefix to use based on VT100 state.
+ * @param first the first character, usually a number.
+ * @param first the last character, one of the following: ~ A B C D F H
+ * @param ctrl whether or not ctrl is down
+ * @param alt whether or not alt is down
+ * @param shift whether or not shift is down
+ * @return the buffer with the full key sequence
+ */
+ private String xtermBuildKeySequence(final String ss3, final char first,
+ final char last, boolean ctrl, boolean alt, boolean shift) {
+
+ StringBuilder sb = new StringBuilder(ss3);
+ if ((last == '~') || (ctrl == true) || (alt == true)
+ || (shift == true)
+ ) {
+ sb.append(first);
+ if ( (ctrl == false) && (alt == false) && (shift == true)) {
+ sb.append(";2");
+ } else if ((ctrl == false) && (alt == true) && (shift == false)) {
+ sb.append(";3");
+ } else if ((ctrl == false) && (alt == true) && (shift == true)) {
+ sb.append(";4");
+ } else if ((ctrl == true) && (alt == false) && (shift == false)) {
+ sb.append(";5");
+ } else if ((ctrl == true) && (alt == false) && (shift == true)) {
+ sb.append(";6");
+ } else if ((ctrl == true) && (alt == true) && (shift == false)) {
+ sb.append(";7");
+ } else if ((ctrl == true) && (alt == true) && (shift == true)) {
+ sb.append(";8");
+ }
+ }
+ sb.append(last);
+ return sb.toString();
+ }
+
+ /**
+ * Translate the keyboard press to a VT100, VT220, or XTERM sequence.
+ *
+ * @param keypress keypress received from the local user
+ * @return string to transmit to the remote side
+ */
+ @SuppressWarnings("fallthrough")
+ private String keypressToString(final TKeypress keypress) {
+
+ if ((fullDuplex == false) && (!keypress.isFnKey())) {
+ /*
+ * If this is a control character, process it like it came from
+ * the remote side.
+ */
+ if (keypress.getChar() < 0x20) {
+ handleControlChar((char) keypress.getChar());
+ } else {
+ // Local echo for everything else
+ printCharacter(keypress.getChar());
+ }
+ if (displayListener != null) {
+ displayListener.displayChanged();
+ }
+ }
+
+ if ((newLineMode == true) && (keypress.equals(kbEnter))) {
+ // NLM: send CRLF
+ return "\015\012";
+ }
+
+ // Handle control characters
+ if ((keypress.isCtrl()) && (!keypress.isFnKey())) {
+ StringBuilder sb = new StringBuilder();
+ int ch = keypress.getChar();
+ ch -= 0x40;
+ sb.append(Character.toChars(ch));
+ return sb.toString();
+ }
+
+ // Handle alt characters
+ if ((keypress.isAlt()) && (!keypress.isFnKey())) {
+ StringBuilder sb = new StringBuilder("\033");
+ int ch = keypress.getChar();
+ sb.append(Character.toChars(ch));
+ return sb.toString();
+ }
+
+ if (keypress.equals(kbBackspaceDel)) {
+ switch (type) {
+ case VT100:
+ return "\010";
+ case VT102:
+ return "\010";
+ case VT220:
+ return "\177";
+ case XTERM:
+ return "\177";
+ }
+ }
+
+ if (keypress.equalsWithoutModifiers(kbLeft)) {
+ switch (type) {
+ case XTERM:
+ switch (arrowKeyMode) {
+ case ANSI:
+ return xtermBuildKeySequence("\033[", '1', 'D',
+ keypress.isCtrl(), keypress.isAlt(),
+ keypress.isShift());
+ case VT52:
+ return xtermBuildKeySequence("\033", '1', 'D',
+ keypress.isCtrl(), keypress.isAlt(),
+ keypress.isShift());
+ case VT100:
+ return xtermBuildKeySequence("\033O", '1', 'D',
+ keypress.isCtrl(), keypress.isAlt(),
+ keypress.isShift());
+ }
+ default:
+ switch (arrowKeyMode) {
+ case ANSI:
+ return "\033[D";
+ case VT52:
+ return "\033D";
+ case VT100:
+ return "\033OD";
+ }
+ }
+ }
+
+ if (keypress.equalsWithoutModifiers(kbRight)) {
+ switch (type) {
+ case XTERM:
+ switch (arrowKeyMode) {
+ case ANSI:
+ return xtermBuildKeySequence("\033[", '1', 'C',
+ keypress.isCtrl(), keypress.isAlt(),
+ keypress.isShift());
+ case VT52:
+ return xtermBuildKeySequence("\033", '1', 'C',
+ keypress.isCtrl(), keypress.isAlt(),
+ keypress.isShift());
+ case VT100:
+ return xtermBuildKeySequence("\033O", '1', 'C',
+ keypress.isCtrl(), keypress.isAlt(),
+ keypress.isShift());
+ }
+ default:
+ switch (arrowKeyMode) {
+ case ANSI:
+ return "\033[C";
+ case VT52:
+ return "\033C";
+ case VT100:
+ return "\033OC";
+ }
+ }
+ }
+
+ if (keypress.equalsWithoutModifiers(kbUp)) {
+ switch (type) {
+ case XTERM:
+ switch (arrowKeyMode) {
+ case ANSI:
+ return xtermBuildKeySequence("\033[", '1', 'A',
+ keypress.isCtrl(), keypress.isAlt(),
+ keypress.isShift());
+ case VT52:
+ return xtermBuildKeySequence("\033", '1', 'A',
+ keypress.isCtrl(), keypress.isAlt(),
+ keypress.isShift());
+ case VT100:
+ return xtermBuildKeySequence("\033O", '1', 'A',
+ keypress.isCtrl(), keypress.isAlt(),
+ keypress.isShift());
+ }
+ default:
+ switch (arrowKeyMode) {
+ case ANSI:
+ return "\033[A";
+ case VT52:
+ return "\033A";
+ case VT100:
+ return "\033OA";
+ }
+ }
+ }
+
+ if (keypress.equalsWithoutModifiers(kbDown)) {
+ switch (type) {
+ case XTERM:
+ switch (arrowKeyMode) {
+ case ANSI:
+ return xtermBuildKeySequence("\033[", '1', 'B',
+ keypress.isCtrl(), keypress.isAlt(),
+ keypress.isShift());
+ case VT52:
+ return xtermBuildKeySequence("\033", '1', 'B',
+ keypress.isCtrl(), keypress.isAlt(),
+ keypress.isShift());
+ case VT100:
+ return xtermBuildKeySequence("\033O", '1', 'B',
+ keypress.isCtrl(), keypress.isAlt(),
+ keypress.isShift());
+ }
+ default:
+ switch (arrowKeyMode) {
+ case ANSI:
+ return "\033[B";
+ case VT52:
+ return "\033B";
+ case VT100:
+ return "\033OB";
+ }
+ }
+ }
+
+ if (keypress.equalsWithoutModifiers(kbHome)) {
+ switch (type) {
+ case XTERM:
+ switch (arrowKeyMode) {
+ case ANSI:
+ return xtermBuildKeySequence("\033[", '1', 'H',
+ keypress.isCtrl(), keypress.isAlt(),
+ keypress.isShift());
+ case VT52:
+ return xtermBuildKeySequence("\033", '1', 'H',
+ keypress.isCtrl(), keypress.isAlt(),
+ keypress.isShift());
+ case VT100:
+ return xtermBuildKeySequence("\033O", '1', 'H',
+ keypress.isCtrl(), keypress.isAlt(),
+ keypress.isShift());
+ }
+ default:
+ switch (arrowKeyMode) {
+ case ANSI:
+ return "\033[H";
+ case VT52:
+ return "\033H";
+ case VT100:
+ return "\033OH";
+ }
+ }
+ }
+
+ if (keypress.equalsWithoutModifiers(kbEnd)) {
+ switch (type) {
+ case XTERM:
+ switch (arrowKeyMode) {
+ case ANSI:
+ return xtermBuildKeySequence("\033[", '1', 'F',
+ keypress.isCtrl(), keypress.isAlt(),
+ keypress.isShift());
+ case VT52:
+ return xtermBuildKeySequence("\033", '1', 'F',
+ keypress.isCtrl(), keypress.isAlt(),
+ keypress.isShift());
+ case VT100:
+ return xtermBuildKeySequence("\033O", '1', 'F',
+ keypress.isCtrl(), keypress.isAlt(),
+ keypress.isShift());
+ }
+ default:
+ switch (arrowKeyMode) {
+ case ANSI:
+ return "\033[F";
+ case VT52:
+ return "\033F";
+ case VT100:
+ return "\033OF";
+ }
+ }
+ }
+
+ if (keypress.equals(kbF1)) {
+ // PF1
+ if (vt52Mode) {
+ return "\033P";
+ }
+ return "\033OP";
+ }
+
+ if (keypress.equals(kbF2)) {
+ // PF2
+ if (vt52Mode) {
+ return "\033Q";
+ }
+ return "\033OQ";
+ }
+
+ if (keypress.equals(kbF3)) {
+ // PF3
+ if (vt52Mode) {
+ return "\033R";
+ }
+ return "\033OR";
+ }
+
+ if (keypress.equals(kbF4)) {
+ // PF4
+ if (vt52Mode) {
+ return "\033S";
+ }
+ return "\033OS";
+ }
+
+ if (keypress.equals(kbF5)) {
+ switch (type) {
+ case VT100:
+ return "\033Ot";
+ case VT102:
+ return "\033Ot";
+ case VT220:
+ return "\033[15~";
+ case XTERM:
+ return "\033[15~";
+ }
+ }
+
+ if (keypress.equals(kbF6)) {
+ switch (type) {
+ case VT100:
+ return "\033Ou";
+ case VT102:
+ return "\033Ou";
+ case VT220:
+ return "\033[17~";
+ case XTERM:
+ return "\033[17~";
+ }
+ }
+
+ if (keypress.equals(kbF7)) {
+ switch (type) {
+ case VT100:
+ return "\033Ov";
+ case VT102:
+ return "\033Ov";
+ case VT220:
+ return "\033[18~";
+ case XTERM:
+ return "\033[18~";
+ }
+ }
+
+ if (keypress.equals(kbF8)) {
+ switch (type) {
+ case VT100:
+ return "\033Ol";
+ case VT102:
+ return "\033Ol";
+ case VT220:
+ return "\033[19~";
+ case XTERM:
+ return "\033[19~";
+ }
+ }
+
+ if (keypress.equals(kbF9)) {
+ switch (type) {
+ case VT100:
+ return "\033Ow";
+ case VT102:
+ return "\033Ow";
+ case VT220:
+ return "\033[20~";
+ case XTERM:
+ return "\033[20~";
+ }
+ }
+
+ if (keypress.equals(kbF10)) {
+ switch (type) {
+ case VT100:
+ return "\033Ox";
+ case VT102:
+ return "\033Ox";
+ case VT220:
+ return "\033[21~";
+ case XTERM:
+ return "\033[21~";
+ }
+ }
+
+ if (keypress.equals(kbF11)) {
+ return "\033[23~";
+ }
+
+ if (keypress.equals(kbF12)) {
+ return "\033[24~";
+ }
+
+ if (keypress.equals(kbShiftF1)) {
+ // Shifted PF1
+ if (vt52Mode) {
+ return "\0332P";
+ }
+ if (type == DeviceType.XTERM) {
+ return "\0331;2P";
+ }
+ return "\033O2P";
+ }
+
+ if (keypress.equals(kbShiftF2)) {
+ // Shifted PF2
+ if (vt52Mode) {
+ return "\0332Q";
+ }
+ if (type == DeviceType.XTERM) {
+ return "\0331;2Q";
+ }
+ return "\033O2Q";
+ }
+
+ if (keypress.equals(kbShiftF3)) {
+ // Shifted PF3
+ if (vt52Mode) {
+ return "\0332R";
+ }
+ if (type == DeviceType.XTERM) {
+ return "\0331;2R";
+ }
+ return "\033O2R";
+ }
+
+ if (keypress.equals(kbShiftF4)) {
+ // Shifted PF4
+ if (vt52Mode) {
+ return "\0332S";
+ }
+ if (type == DeviceType.XTERM) {
+ return "\0331;2S";
+ }
+ return "\033O2S";
+ }
+
+ if (keypress.equals(kbShiftF5)) {
+ // Shifted F5
+ return "\033[15;2~";
+ }
+
+ if (keypress.equals(kbShiftF6)) {
+ // Shifted F6
+ return "\033[17;2~";
+ }
+
+ if (keypress.equals(kbShiftF7)) {
+ // Shifted F7
+ return "\033[18;2~";
+ }
+
+ if (keypress.equals(kbShiftF8)) {
+ // Shifted F8
+ return "\033[19;2~";
+ }
+
+ if (keypress.equals(kbShiftF9)) {
+ // Shifted F9
+ return "\033[20;2~";
+ }
+
+ if (keypress.equals(kbShiftF10)) {
+ // Shifted F10
+ return "\033[21;2~";
+ }
+
+ if (keypress.equals(kbShiftF11)) {
+ // Shifted F11
+ return "\033[23;2~";
+ }
+
+ if (keypress.equals(kbShiftF12)) {
+ // Shifted F12
+ return "\033[24;2~";
+ }
+
+ if (keypress.equals(kbCtrlF1)) {
+ // Control PF1
+ if (vt52Mode) {
+ return "\0335P";
+ }
+ if (type == DeviceType.XTERM) {
+ return "\0331;5P";
+ }
+ return "\033O5P";
+ }
+
+ if (keypress.equals(kbCtrlF2)) {
+ // Control PF2
+ if (vt52Mode) {
+ return "\0335Q";
+ }
+ if (type == DeviceType.XTERM) {
+ return "\0331;5Q";
+ }
+ return "\033O5Q";
+ }
+
+ if (keypress.equals(kbCtrlF3)) {
+ // Control PF3
+ if (vt52Mode) {
+ return "\0335R";
+ }
+ if (type == DeviceType.XTERM) {
+ return "\0331;5R";
+ }
+ return "\033O5R";
+ }
+
+ if (keypress.equals(kbCtrlF4)) {
+ // Control PF4
+ if (vt52Mode) {
+ return "\0335S";
+ }
+ if (type == DeviceType.XTERM) {
+ return "\0331;5S";
+ }
+ return "\033O5S";
+ }
+
+ if (keypress.equals(kbCtrlF5)) {
+ // Control F5
+ return "\033[15;5~";
+ }
+
+ if (keypress.equals(kbCtrlF6)) {
+ // Control F6
+ return "\033[17;5~";
+ }
+
+ if (keypress.equals(kbCtrlF7)) {
+ // Control F7
+ return "\033[18;5~";
+ }
+
+ if (keypress.equals(kbCtrlF8)) {
+ // Control F8
+ return "\033[19;5~";
+ }
+
+ if (keypress.equals(kbCtrlF9)) {
+ // Control F9
+ return "\033[20;5~";
+ }
+
+ if (keypress.equals(kbCtrlF10)) {
+ // Control F10
+ return "\033[21;5~";
+ }
+
+ if (keypress.equals(kbCtrlF11)) {
+ // Control F11
+ return "\033[23;5~";
+ }
+
+ if (keypress.equals(kbCtrlF12)) {
+ // Control F12
+ return "\033[24;5~";
+ }
+
+ if (keypress.equalsWithoutModifiers(kbPgUp)) {
+ switch (type) {
+ case XTERM:
+ return xtermBuildKeySequence("\033[", '5', '~',
+ keypress.isCtrl(), keypress.isAlt(),
+ keypress.isShift());
+ default:
+ return "\033[5~";
+ }
+ }
+
+ if (keypress.equalsWithoutModifiers(kbPgDn)) {
+ switch (type) {
+ case XTERM:
+ return xtermBuildKeySequence("\033[", '6', '~',
+ keypress.isCtrl(), keypress.isAlt(),
+ keypress.isShift());
+ default:
+ return "\033[6~";
+ }
+ }
+
+ if (keypress.equalsWithoutModifiers(kbIns)) {
+ switch (type) {
+ case XTERM:
+ return xtermBuildKeySequence("\033[", '2', '~',
+ keypress.isCtrl(), keypress.isAlt(),
+ keypress.isShift());
+ default:
+ return "\033[2~";
+ }
+ }
+
+ if (keypress.equalsWithoutModifiers(kbDel)) {
+ switch (type) {
+ case XTERM:
+ return xtermBuildKeySequence("\033[", '3', '~',
+ keypress.isCtrl(), keypress.isAlt(),
+ keypress.isShift());
+ default:
+ // Delete sends real delete for VTxxx
+ return "\177";
+ }
+ }
+
+ if (keypress.equals(kbEnter)) {
+ return "\015";
+ }
+
+ if (keypress.equals(kbEsc)) {
+ return "\033";
+ }
+
+ if (keypress.equals(kbAltEsc)) {
+ return "\033\033";
+ }
+
+ if (keypress.equals(kbTab)) {
+ return "\011";
+ }
+
+ if ((keypress.equalsWithoutModifiers(kbBackTab)) ||
+ (keypress.equals(kbShiftTab))
+ ) {
+ switch (type) {
+ case XTERM:
+ return "\033[Z";
+ default:
+ return "\011";
+ }
+ }
+
+ // Non-alt, non-ctrl characters
+ if (!keypress.isFnKey()) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(Character.toChars(keypress.getChar()));
+ return sb.toString();
+ }
+ return "";
+ }
+
+ /**
+ * Map a symbol in any one of the VT100/VT220 character sets to a Unicode
+ * symbol.
+ *
+ * @param ch 8-bit character from the remote side
+ * @param charsetGl character set defined for GL
+ * @param charsetGr character set defined for GR
+ * @return character to display on the screen
+ */
+ private char mapCharacterCharset(final int ch,
+ final CharacterSet charsetGl,
+ final CharacterSet charsetGr) {
+
+ int lookupChar = ch;
+ CharacterSet lookupCharset = charsetGl;
+
+ if (ch >= 0x80) {
+ assert ((type == DeviceType.VT220) || (type == DeviceType.XTERM));
+ lookupCharset = charsetGr;
+ lookupChar &= 0x7F;
+ }
+
+ switch (lookupCharset) {
+
+ case DRAWING:
+ return DECCharacterSets.SPECIAL_GRAPHICS[lookupChar];
+
+ case UK:
+ return DECCharacterSets.UK[lookupChar];
+
+ case US:
+ return DECCharacterSets.US_ASCII[lookupChar];
+
+ case NRC_DUTCH:
+ return DECCharacterSets.NL[lookupChar];
+
+ case NRC_FINNISH:
+ return DECCharacterSets.FI[lookupChar];
+
+ case NRC_FRENCH:
+ return DECCharacterSets.FR[lookupChar];
+
+ case NRC_FRENCH_CA:
+ return DECCharacterSets.FR_CA[lookupChar];
+
+ case NRC_GERMAN:
+ return DECCharacterSets.DE[lookupChar];
+
+ case NRC_ITALIAN:
+ return DECCharacterSets.IT[lookupChar];
+
+ case NRC_NORWEGIAN:
+ return DECCharacterSets.NO[lookupChar];
+
+ case NRC_SPANISH:
+ return DECCharacterSets.ES[lookupChar];
+
+ case NRC_SWEDISH:
+ return DECCharacterSets.SV[lookupChar];
+
+ case NRC_SWISS:
+ return DECCharacterSets.SWISS[lookupChar];
+
+ case DEC_SUPPLEMENTAL:
+ return DECCharacterSets.DEC_SUPPLEMENTAL[lookupChar];
+
+ case VT52_GRAPHICS:
+ return DECCharacterSets.VT52_SPECIAL_GRAPHICS[lookupChar];
+
+ case ROM:
+ return DECCharacterSets.US_ASCII[lookupChar];
+
+ case ROM_SPECIAL:
+ return DECCharacterSets.US_ASCII[lookupChar];
+
+ default:
+ throw new IllegalArgumentException("Invalid character set value: "
+ + lookupCharset);
+ }
+ }
+
+ /**
+ * Map an 8-bit byte into a printable character.
+ *
+ * @param ch either 8-bit or Unicode character from the remote side
+ * @return character to display on the screen
+ */
+ private int mapCharacter(final int ch) {
+ if (ch >= 0x100) {
+ // Unicode character, just return it
+ return ch;
+ }
+
+ CharacterSet charsetGl = currentState.g0Charset;
+ CharacterSet charsetGr = currentState.grCharset;
+
+ if (vt52Mode == true) {
+ if (shiftOut == true) {
+ // Shifted out character, pull from VT52 graphics
+ charsetGl = currentState.g1Charset;
+ charsetGr = CharacterSet.US;
+ } else {
+ // Normal
+ charsetGl = currentState.g0Charset;
+ charsetGr = CharacterSet.US;
+ }
+
+ // Pull the character
+ return mapCharacterCharset(ch, charsetGl, charsetGr);
+ }
+
+ // shiftOout
+ if (shiftOut == true) {
+ // Shifted out character, pull from G1
+ charsetGl = currentState.g1Charset;
+ charsetGr = currentState.grCharset;
+
+ // Pull the character
+ return mapCharacterCharset(ch, charsetGl, charsetGr);
+ }
+
+ // SS2
+ if (singleshift == Singleshift.SS2) {
+
+ singleshift = Singleshift.NONE;
+
+ // Shifted out character, pull from G2
+ charsetGl = currentState.g2Charset;
+ charsetGr = currentState.grCharset;
+ }
+
+ // SS3
+ if (singleshift == Singleshift.SS3) {
+
+ singleshift = Singleshift.NONE;
+
+ // Shifted out character, pull from G3
+ charsetGl = currentState.g3Charset;
+ charsetGr = currentState.grCharset;
+ }
+
+ if ((type == DeviceType.VT220) || (type == DeviceType.XTERM)) {
+ // Check for locking shift
+
+ switch (currentState.glLockshift) {
+
+ case G1_GR:
+ throw new IllegalArgumentException("programming bug");
+
+ case G2_GR:
+ throw new IllegalArgumentException("programming bug");
+
+ case G3_GR:
+ throw new IllegalArgumentException("programming bug");
+
+ case G2_GL:
+ // LS2
+ charsetGl = currentState.g2Charset;
+ break;
+
+ case G3_GL:
+ // LS3
+ charsetGl = currentState.g3Charset;
+ break;
+
+ case NONE:
+ // Normal
+ charsetGl = currentState.g0Charset;
+ break;
+ }
+
+ switch (currentState.grLockshift) {
+
+ case G2_GL:
+ throw new IllegalArgumentException("programming bug");
+
+ case G3_GL:
+ throw new IllegalArgumentException("programming bug");
+
+ case G1_GR:
+ // LS1R
+ charsetGr = currentState.g1Charset;
+ break;
+
+ case G2_GR:
+ // LS2R
+ charsetGr = currentState.g2Charset;
+ break;
+
+ case G3_GR:
+ // LS3R
+ charsetGr = currentState.g3Charset;
+ break;
+
+ case NONE:
+ // Normal
+ charsetGr = CharacterSet.DEC_SUPPLEMENTAL;
+ break;
+ }
+
+
+ }
+
+ // Pull the character
+ return mapCharacterCharset(ch, charsetGl, charsetGr);
+ }
+
+ /**
+ * Scroll the text within a scrolling region up n lines.
+ *
+ * @param regionTop top row of the scrolling region
+ * @param regionBottom bottom row of the scrolling region
+ * @param n number of lines to scroll
+ */
+ private void scrollingRegionScrollUp(final int regionTop,
+ final int regionBottom, final int n) {
+
+ if (regionTop >= regionBottom) {
+ return;
+ }
+
+ // Sanity check: see if there will be any characters left after the
+ // scroll
+ if (regionBottom + 1 - regionTop <= n) {
+ // There won't be anything left in the region, so just call
+ // eraseScreen() and return.
+ eraseScreen(regionTop, 0, regionBottom, width - 1, false);
+ return;
+ }
+
+ int remaining = regionBottom + 1 - regionTop - n;
+ List<DisplayLine> displayTop = display.subList(0, regionTop);
+ List<DisplayLine> displayBottom = display.subList(regionBottom + 1,
+ display.size());
+ List<DisplayLine> displayMiddle = display.subList(regionBottom + 1
+ - remaining, regionBottom + 1);
+ display = new ArrayList<DisplayLine>(displayTop);
+ display.addAll(displayMiddle);
+ for (int i = 0; i < n; i++) {
+ DisplayLine line = new DisplayLine(currentState.attr);
+ line.setReverseColor(reverseVideo);
+ display.add(line);
+ }
+ display.addAll(displayBottom);
+
+ assert (display.size() == height);
+ }
+
+ /**
+ * Scroll the text within a scrolling region down n lines.
+ *
+ * @param regionTop top row of the scrolling region
+ * @param regionBottom bottom row of the scrolling region
+ * @param n number of lines to scroll
+ */
+ private void scrollingRegionScrollDown(final int regionTop,
+ final int regionBottom, final int n) {
+
+ if (regionTop >= regionBottom) {
+ return;
+ }
+
+ // Sanity check: see if there will be any characters left after the
+ // scroll
+ if (regionBottom + 1 - regionTop <= n) {
+ // There won't be anything left in the region, so just call
+ // eraseScreen() and return.
+ eraseScreen(regionTop, 0, regionBottom, width - 1, false);
+ return;
+ }
+
+ int remaining = regionBottom + 1 - regionTop - n;
+ List<DisplayLine> displayTop = display.subList(0, regionTop);
+ List<DisplayLine> displayBottom = display.subList(regionBottom + 1,
+ display.size());
+ List<DisplayLine> displayMiddle = display.subList(regionTop,
+ regionTop + remaining);
+ display = new ArrayList<DisplayLine>(displayTop);
+ for (int i = 0; i < n; i++) {
+ DisplayLine line = new DisplayLine(currentState.attr);
+ line.setReverseColor(reverseVideo);
+ display.add(line);
+ }
+ display.addAll(displayMiddle);
+ display.addAll(displayBottom);
+
+ assert (display.size() == height);
+ }
+
+ /**
+ * Process a control character.
+ *
+ * @param ch 8-bit character from the remote side
+ */
+ private void handleControlChar(final char ch) {
+ assert ((ch <= 0x1F) || ((ch >= 0x7F) && (ch <= 0x9F)));
+
+ switch (ch) {
+
+ case 0x00:
+ // NUL - discard
+ return;
+
+ case 0x05:
+ // ENQ
+
+ // Transmit the answerback message.
+ // Not supported
+ break;
+
+ case 0x07:
+ // BEL
+ // Not supported
+ break;
+
+ case 0x08:
+ // BS
+ cursorLeft(1, false);
+ break;
+
+ case 0x09:
+ // HT
+ advanceToNextTabStop();
+ break;
+
+ case 0x0A:
+ // LF
+ linefeed();
+ break;
+
+ case 0x0B:
+ // VT
+ linefeed();
+ break;
+
+ case 0x0C:
+ // FF
+ linefeed();
+ break;
+
+ case 0x0D:
+ // CR
+ carriageReturn();
+ break;
+
+ case 0x0E:
+ // SO
+ shiftOut = true;
+ currentState.glLockshift = LockshiftMode.NONE;
+ break;
+
+ case 0x0F:
+ // SI
+ shiftOut = false;
+ currentState.glLockshift = LockshiftMode.NONE;
+ break;
+
+ case 0x84:
+ // IND
+ ind();
+ break;
+
+ case 0x85:
+ // NEL
+ nel();
+ break;
+
+ case 0x88:
+ // HTS
+ hts();
+ break;
+
+ case 0x8D:
+ // RI
+ ri();
+ break;
+
+ case 0x8E:
+ // SS2
+ singleshift = Singleshift.SS2;
+ break;
+
+ case 0x8F:
+ // SS3
+ singleshift = Singleshift.SS3;
+ break;
+
+ default:
+ break;
+ }
+
+ }
+
+ /**
+ * Advance the cursor to the next tab stop.
+ */
+ private void advanceToNextTabStop() {
+ if (tabStops.size() == 0) {
+ // Go to the rightmost column
+ cursorRight(rightMargin - currentState.cursorX, false);
+ return;
+ }
+ for (Integer stop: tabStops) {
+ if (stop > currentState.cursorX) {
+ cursorRight(stop - currentState.cursorX, false);
+ return;
+ }
+ }
+ /*
+ * We got here, meaning there isn't a tab stop beyond the current
+ * cursor position. Place the cursor of the right-most edge of the
+ * screen.
+ */
+ cursorRight(rightMargin - currentState.cursorX, false);
+ }
+
+ /**
+ * Save a character into the collect buffer.
+ *
+ * @param ch character to save
+ */
+ private void collect(final char ch) {
+ collectBuffer.append(ch);
+ }
+
+ /**
+ * Save a byte into the CSI parameters buffer.
+ *
+ * @param ch byte to save
+ */
+ private void param(final byte ch) {
+ if (csiParams.size() == 0) {
+ csiParams.add(Integer.valueOf(0));
+ }
+ Integer x = csiParams.get(csiParams.size() - 1);
+ if ((ch >= '0') && (ch <= '9')) {
+ x *= 10;
+ x += (ch - '0');
+ csiParams.set(csiParams.size() - 1, x);
+ }
+
+ if ((ch == ';') && (csiParams.size() < 16)) {
+ csiParams.add(Integer.valueOf(0));
+ }
+ }
+
+ /**
+ * Get a CSI parameter value, with a default.
+ *
+ * @param position parameter index. 0 is the first parameter.
+ * @param defaultValue value to use if csiParams[position] doesn't exist
+ * @return parameter value
+ */
+ private int getCsiParam(final int position, final int defaultValue) {
+ if (csiParams.size() < position + 1) {
+ return defaultValue;
+ }
+ return csiParams.get(position).intValue();
+ }
+
+ /**
+ * Get a CSI parameter value, clamped to within min/max.
+ *
+ * @param position parameter index. 0 is the first parameter.
+ * @param defaultValue value to use if csiParams[position] doesn't exist
+ * @param minValue minimum value inclusive
+ * @param maxValue maximum value inclusive
+ * @return parameter value
+ */
+ private int getCsiParam(final int position, final int defaultValue,
+ final int minValue, final int maxValue) {
+
+ assert (minValue <= maxValue);
+ int value = getCsiParam(position, defaultValue);
+ if (value < minValue) {
+ value = minValue;
+ }
+ if (value > maxValue) {
+ value = maxValue;
+ }
+ return value;
+ }
+
+ /**
+ * Set or unset a toggle.
+ *
+ * @param value true for set ('h'), false for reset ('l')
+ */
+ private void setToggle(final boolean value) {
+ boolean decPrivateModeFlag = false;
+
+ for (int i = 0; i < collectBuffer.length(); i++) {
+ if (collectBuffer.charAt(i) == '?') {
+ decPrivateModeFlag = true;
+ break;
+ }
+ }
+
+ for (Integer i: csiParams) {
+
+ switch (i) {
+
+ case 1:
+ if (decPrivateModeFlag == true) {
+ // DECCKM
+ if (value == true) {
+ // Use application arrow keys
+ arrowKeyMode = ArrowKeyMode.VT100;
+ } else {
+ // Use ANSI arrow keys
+ arrowKeyMode = ArrowKeyMode.ANSI;
+ }
+ }
+ break;
+ case 2:
+ if (decPrivateModeFlag == true) {
+ if (value == false) {
+
+ // DECANM
+ vt52Mode = true;
+ arrowKeyMode = ArrowKeyMode.VT52;
+
+ /*
+ * From the VT102 docs: "You use ANSI mode to select
+ * most terminal features; the terminal uses the same
+ * features when it switches to VT52 mode. You
+ * cannot, however, change most of these features in
+ * VT52 mode."
+ *
+ * In other words, do not reset any other attributes
+ * when switching between VT52 submode and ANSI.
+ *
+ * HOWEVER, the real vt100 does switch the character
+ * set according to Usenet.
+ */
+ currentState.g0Charset = CharacterSet.US;
+ currentState.g1Charset = CharacterSet.DRAWING;
+ shiftOut = false;
+
+ if ((type == DeviceType.VT220)
+ || (type == DeviceType.XTERM)) {
+
+ // VT52 mode is explicitly 7-bit
+ s8c1t = false;
+ singleshift = Singleshift.NONE;
+ }
+ }
+ } else {
+ // KAM
+ if (value == true) {
+ // Turn off keyboard
+ // Not supported
+ } else {
+ // Turn on keyboard
+ // Not supported
+ }
+ }
+ break;
+ case 3:
+ if (decPrivateModeFlag == true) {
+ // DECCOLM
+ if (value == true) {
+ // 132 columns
+ columns132 = true;
+ rightMargin = 131;
+ } else {
+ // 80 columns
+ columns132 = false;
+ if ((displayListener != null)
+ && (type == DeviceType.XTERM)
+ ) {
+ // For xterms, reset to the actual width, not 80
+ // columns.
+ width = displayListener.getDisplayWidth();
+ rightMargin = width - 1;
+ } else {
+ rightMargin = 79;
+ width = rightMargin + 1;
+ }
+ }
+ // Entire screen is cleared, and scrolling region is
+ // reset
+ eraseScreen(0, 0, height - 1, width - 1, false);
+ scrollRegionTop = 0;
+ scrollRegionBottom = height - 1;
+ // Also home the cursor
+ cursorPosition(0, 0);
+ }
+ break;
+ case 4:
+ if (decPrivateModeFlag == true) {
+ // DECSCLM
+ if (value == true) {
+ // Smooth scroll
+ // Not supported
+ } else {
+ // Jump scroll
+ // Not supported
+ }
+ } else {
+ // IRM
+ if (value == true) {
+ insertMode = true;
+ } else {
+ insertMode = false;
+ }
+ }
+ break;
+ case 5:
+ if (decPrivateModeFlag == true) {
+ // DECSCNM
+ if (value == true) {
+ /*
+ * Set selects reverse screen, a white screen
+ * background with black characters.
+ */
+ if (reverseVideo != true) {
+ /*
+ * If in normal video, switch it back
+ */
+ invertDisplayColors();
+ }
+ reverseVideo = true;
+ } else {
+ /*
+ * Reset selects normal screen, a black screen
+ * background with white characters.
+ */
+ if (reverseVideo == true) {
+ /*
+ * If in reverse video already, switch it back
+ */
+ invertDisplayColors();
+ }
+ reverseVideo = false;
+ }
+ }
+ break;
+ case 6:
+ if (decPrivateModeFlag == true) {
+ // DECOM
+ if (value == true) {
+ // Origin is relative to scroll region cursor.
+ // Cursor can NEVER leave scrolling region.
+ currentState.originMode = true;
+ cursorPosition(0, 0);
+ } else {
+ // Origin is absolute to entire screen. Cursor can
+ // leave the scrolling region via cup() and hvp().
+ currentState.originMode = false;
+ cursorPosition(0, 0);
+ }
+ }
+ break;
+ case 7:
+ if (decPrivateModeFlag == true) {
+ // DECAWM
+ if (value == true) {
+ // Turn linewrap on
+ currentState.lineWrap = true;
+ } else {
+ // Turn linewrap off
+ currentState.lineWrap = false;
+ }
+ }
+ break;
+ case 8:
+ if (decPrivateModeFlag == true) {
+ // DECARM
+ if (value == true) {
+ // Keyboard auto-repeat on
+ // Not supported
+ } else {
+ // Keyboard auto-repeat off
+ // Not supported
+ }
+ }
+ break;
+ case 12:
+ if (decPrivateModeFlag == false) {
+ // SRM
+ if (value == true) {
+ // Local echo off
+ fullDuplex = true;
+ } else {
+ // Local echo on
+ fullDuplex = false;
+ }
+ }
+ break;
+ case 18:
+ if (decPrivateModeFlag == true) {
+ // DECPFF
+ // Not supported
+ }
+ break;
+ case 19:
+ if (decPrivateModeFlag == true) {
+ // DECPEX
+ // Not supported
+ }
+ break;
+ case 20:
+ if (decPrivateModeFlag == false) {
+ // LNM
+ if (value == true) {
+ /*
+ * Set causes a received linefeed, form feed, or
+ * vertical tab to move cursor to first column of
+ * next line. RETURN transmits both a carriage return
+ * and linefeed. This selection is also called new
+ * line option.
+ */
+ newLineMode = true;
+ } else {
+ /*
+ * Reset causes a received linefeed, form feed, or
+ * vertical tab to move cursor to next line in
+ * current column. RETURN transmits a carriage
+ * return.
+ */
+ newLineMode = false;
+ }
+ }
+ break;
+
+ case 25:
+ if ((type == DeviceType.VT220) || (type == DeviceType.XTERM)) {
+ if (decPrivateModeFlag == true) {
+ // DECTCEM
+ if (value == true) {
+ // Visible cursor
+ cursorVisible = true;
+ } else {
+ // Invisible cursor
+ cursorVisible = false;
+ }
+ }
+ }
+ break;
+
+ case 42:
+ if ((type == DeviceType.VT220) || (type == DeviceType.XTERM)) {
+ if (decPrivateModeFlag == true) {
+ // DECNRCM
+ if (value == true) {
+ // Select national mode NRC
+ // Not supported
+ } else {
+ // Select multi-national mode
+ // Not supported
+ }
+ }
+ }
+
+ break;
+
+ case 80:
+ if (type == DeviceType.XTERM) {
+ if (decPrivateModeFlag == true) {
+ if (value == true) {
+ // Enable sixel scrolling (default).
+ // TODO
+ } else {
+ // Disable sixel scrolling.
+ // TODO
+ }
+ }
+ }
+
+ break;
+
+ case 1000:
+ if ((type == DeviceType.XTERM)
+ && (decPrivateModeFlag == true)
+ ) {
+ // Mouse: normal tracking mode
+ if (value == true) {
+ mouseProtocol = MouseProtocol.NORMAL;
+ } else {
+ mouseProtocol = MouseProtocol.OFF;
+ }
+ }
+ break;
+
+ case 1002:
+ if ((type == DeviceType.XTERM)
+ && (decPrivateModeFlag == true)
+ ) {
+ // Mouse: normal tracking mode
+ if (value == true) {
+ mouseProtocol = MouseProtocol.BUTTONEVENT;
+ } else {
+ mouseProtocol = MouseProtocol.OFF;
+ }
+ }
+ break;
+
+ case 1003:
+ if ((type == DeviceType.XTERM)
+ && (decPrivateModeFlag == true)
+ ) {
+ // Mouse: Any-event tracking mode
+ if (value == true) {
+ mouseProtocol = MouseProtocol.ANYEVENT;
+ } else {
+ mouseProtocol = MouseProtocol.OFF;
+ }
+ }
+ break;
+
+ case 1005:
+ if ((type == DeviceType.XTERM)
+ && (decPrivateModeFlag == true)
+ ) {
+ // Mouse: UTF-8 coordinates
+ if (value == true) {
+ mouseEncoding = MouseEncoding.UTF8;
+ } else {
+ mouseEncoding = MouseEncoding.X10;
+ }
+ }
+ break;
+
+ case 1006:
+ if ((type == DeviceType.XTERM)
+ && (decPrivateModeFlag == true)
+ ) {
+ // Mouse: SGR coordinates
+ if (value == true) {
+ mouseEncoding = MouseEncoding.SGR;
+ } else {
+ mouseEncoding = MouseEncoding.X10;
+ }
+ }
+ break;
+
+ case 1070:
+ if (type == DeviceType.XTERM) {
+ if (decPrivateModeFlag == true) {
+ if (value == true) {
+ // Use private color registers for each sixel
+ // graphic (default).
+ sixelPalette = null;
+ } else {
+ // Use shared color registers for each sixel
+ // graphic.
+ sixelPalette = new HashMap<Integer, java.awt.Color>();
+ }
+ }
+ }
+ break;
+
+ default:
+ break;
+
+ }
+ }
+ }
+
+ /**
+ * DECSC - Save cursor.
+ */
+ private void decsc() {
+ savedState.setTo(currentState);
+ }
+
+ /**
+ * DECRC - Restore cursor.
+ */
+ private void decrc() {
+ currentState.setTo(savedState);
+ }
+
+ /**
+ * IND - Index.
+ */
+ private void ind() {
+ // Move the cursor and scroll if necessary. If at the bottom line
+ // already, a scroll up is supposed to be performed.
+ if (currentState.cursorY == scrollRegionBottom) {
+ scrollingRegionScrollUp(scrollRegionTop, scrollRegionBottom, 1);
+ }
+ cursorDown(1, true);
+ }
+
+ /**
+ * RI - Reverse index.
+ */
+ private void ri() {
+ // Move the cursor and scroll if necessary. If at the top line
+ // already, a scroll down is supposed to be performed.
+ if (currentState.cursorY == scrollRegionTop) {
+ scrollingRegionScrollDown(scrollRegionTop, scrollRegionBottom, 1);
+ }
+ cursorUp(1, true);
+ }
+
+ /**
+ * NEL - Next line.
+ */
+ private void nel() {
+ // Move the cursor and scroll if necessary. If at the bottom line
+ // already, a scroll up is supposed to be performed.
+ if (currentState.cursorY == scrollRegionBottom) {
+ scrollingRegionScrollUp(scrollRegionTop, scrollRegionBottom, 1);
+ }
+ cursorDown(1, true);
+
+ // Reset to the beginning of the next line
+ currentState.cursorX = 0;
+ }
+
+ /**
+ * DECKPAM - Keypad application mode.
+ */
+ private void deckpam() {
+ keypadMode = KeypadMode.Application;
+ }
+
+ /**
+ * DECKPNM - Keypad numeric mode.
+ */
+ private void deckpnm() {
+ keypadMode = KeypadMode.Numeric;
+ }
+
+ /**
+ * Move up n spaces.
+ *
+ * @param n number of spaces to move
+ * @param honorScrollRegion if true, then do nothing if the cursor is
+ * outside the scrolling region
+ */
+ private void cursorUp(final int n, final boolean honorScrollRegion) {
+ int top;
+
+ /*
+ * Special case: if a user moves the cursor from the right margin, we
+ * have to reset the VT100 right margin flag.
+ */
+ if (n > 0) {
+ wrapLineFlag = false;
+ }
+
+ for (int i = 0; i < n; i++) {
+ if (honorScrollRegion == true) {
+ // Honor the scrolling region
+ if ((currentState.cursorY < scrollRegionTop)
+ || (currentState.cursorY > scrollRegionBottom)
+ ) {
+ // Outside region, do nothing
+ return;
+ }
+ // Inside region, go up
+ top = scrollRegionTop;
+ } else {
+ // Non-scrolling case
+ top = 0;
+ }
+
+ if (currentState.cursorY > top) {
+ currentState.cursorY--;
+ }
+ }
+ }
+
+ /**
+ * Move down n spaces.
+ *
+ * @param n number of spaces to move
+ * @param honorScrollRegion if true, then do nothing if the cursor is
+ * outside the scrolling region
+ */
+ private void cursorDown(final int n, final boolean honorScrollRegion) {
+ int bottom;
+
+ /*
+ * Special case: if a user moves the cursor from the right margin, we
+ * have to reset the VT100 right margin flag.
+ */
+ if (n > 0) {
+ wrapLineFlag = false;
+ }
+
+ for (int i = 0; i < n; i++) {
+
+ if (honorScrollRegion == true) {
+ // Honor the scrolling region
+ if (currentState.cursorY > scrollRegionBottom) {
+ // Outside region, do nothing
+ return;
+ }
+ // Inside region, go down
+ bottom = scrollRegionBottom;
+ } else {
+ // Non-scrolling case
+ bottom = height - 1;
+ }
+
+ if (currentState.cursorY < bottom) {
+ currentState.cursorY++;
+ }
+ }
+ }
+
+ /**
+ * Move left n spaces.
+ *
+ * @param n number of spaces to move
+ * @param honorScrollRegion if true, then do nothing if the cursor is
+ * outside the scrolling region
+ */
+ private void cursorLeft(final int n, final boolean honorScrollRegion) {
+ /*
+ * Special case: if a user moves the cursor from the right margin, we
+ * have to reset the VT100 right margin flag.
+ */
+ if (n > 0) {
+ wrapLineFlag = false;
+ }
+
+ for (int i = 0; i < n; i++) {
+ if (honorScrollRegion == true) {
+ // Honor the scrolling region
+ if ((currentState.cursorY < scrollRegionTop)
+ || (currentState.cursorY > scrollRegionBottom)
+ ) {
+ // Outside region, do nothing
+ return;
+ }
+ }
+
+ if (currentState.cursorX > 0) {
+ currentState.cursorX--;
+ }
+ }
+ }
+
+ /**
+ * Move right n spaces.
+ *
+ * @param n number of spaces to move
+ * @param honorScrollRegion if true, then do nothing if the cursor is
+ * outside the scrolling region
+ */
+ private void cursorRight(final int n, final boolean honorScrollRegion) {
+ int rightMargin = this.rightMargin;
+
+ /*
+ * Special case: if a user moves the cursor from the right margin, we
+ * have to reset the VT100 right margin flag.
+ */
+ if (n > 0) {
+ wrapLineFlag = false;
+ }
+
+ if (display.get(currentState.cursorY).isDoubleWidth()) {
+ rightMargin = ((rightMargin + 1) / 2) - 1;
+ }
+
+ for (int i = 0; i < n; i++) {
+ if (honorScrollRegion == true) {
+ // Honor the scrolling region
+ if ((currentState.cursorY < scrollRegionTop)
+ || (currentState.cursorY > scrollRegionBottom)
+ ) {
+ // Outside region, do nothing
+ return;
+ }
+ }
+
+ if (currentState.cursorX < rightMargin) {
+ currentState.cursorX++;
+ }
+ }
+ }
+
+ /**
+ * Move cursor to (col, row) where (0, 0) is the top-left corner.
+ *
+ * @param row row to move to
+ * @param col column to move to
+ */
+ private void cursorPosition(int row, final int col) {
+ int rightMargin = this.rightMargin;
+
+ assert (col >= 0);
+ assert (row >= 0);
+
+ if (display.get(currentState.cursorY).isDoubleWidth()) {
+ rightMargin = ((rightMargin + 1) / 2) - 1;
+ }
+
+ // Set column number
+ currentState.cursorX = col;
+
+ // Sanity check, bring column back to margin.
+ if (currentState.cursorX > rightMargin) {
+ currentState.cursorX = rightMargin;
+ }
+
+ // Set row number
+ if (currentState.originMode == true) {
+ row += scrollRegionTop;
+ }
+ if (currentState.cursorY < row) {
+ cursorDown(row - currentState.cursorY, false);
+ } else if (currentState.cursorY > row) {
+ cursorUp(currentState.cursorY - row, false);
+ }
+
+ wrapLineFlag = false;
+ }
+
+ /**
+ * HTS - Horizontal tabulation set.
+ */
+ private void hts() {
+ for (Integer stop: tabStops) {
+ if (stop == currentState.cursorX) {
+ // Already have a tab stop here
+ return;
+ }
+ }
+
+ // Append a tab stop to the end of the array and resort them
+ tabStops.add(currentState.cursorX);
+ Collections.sort(tabStops);
+ }
+
+ /**
+ * DECSWL - Single-width line.
+ */
+ private void decswl() {
+ display.get(currentState.cursorY).setDoubleWidth(false);
+ display.get(currentState.cursorY).setDoubleHeight(0);
+ }
+
+ /**
+ * DECDWL - Double-width line.
+ */
+ private void decdwl() {
+ display.get(currentState.cursorY).setDoubleWidth(true);
+ display.get(currentState.cursorY).setDoubleHeight(0);
+ }
+
+ /**
+ * DECHDL - Double-height + double-width line.
+ *
+ * @param topHalf if true, this sets the row to be the top half row of a
+ * double-height row
+ */
+ private void dechdl(final boolean topHalf) {
+ display.get(currentState.cursorY).setDoubleWidth(true);
+ if (topHalf == true) {
+ display.get(currentState.cursorY).setDoubleHeight(1);
+ } else {
+ display.get(currentState.cursorY).setDoubleHeight(2);
+ }
+ }
+
+ /**
+ * DECALN - Screen alignment display.
+ */
+ private void decaln() {
+ Cell newCell = new Cell('E');
+ for (DisplayLine line: display) {
+ for (int i = 0; i < line.length(); i++) {
+ line.replace(i, newCell);
+ }
+ }
+ }
+
+ /**
+ * DECSCL - Compatibility level.
+ */
+ private void decscl() {
+ int i = getCsiParam(0, 0);
+ int j = getCsiParam(1, 0);
+
+ if (i == 61) {
+ // Reset fonts
+ currentState.g0Charset = CharacterSet.US;
+ currentState.g1Charset = CharacterSet.DRAWING;
+ s8c1t = false;
+ } else if (i == 62) {
+
+ if ((j == 0) || (j == 2)) {
+ s8c1t = true;
+ } else if (j == 1) {
+ s8c1t = false;
+ }
+ }
+ }
+
+ /**
+ * CUD - Cursor down.
+ */
+ private void cud() {
+ cursorDown(getCsiParam(0, 1, 1, height), true);
+ }
+
+ /**
+ * CUF - Cursor forward.
+ */
+ private void cuf() {
+ cursorRight(getCsiParam(0, 1, 1, rightMargin + 1), true);
+ }
+
+ /**
+ * CUB - Cursor backward.
+ */
+ private void cub() {
+ cursorLeft(getCsiParam(0, 1, 1, currentState.cursorX + 1), true);
+ }
+
+ /**
+ * CUU - Cursor up.
+ */
+ private void cuu() {
+ cursorUp(getCsiParam(0, 1, 1, currentState.cursorY + 1), true);
+ }
+
+ /**
+ * CUP - Cursor position.
+ */
+ private void cup() {
+ cursorPosition(getCsiParam(0, 1, 1, height) - 1,
+ getCsiParam(1, 1, 1, rightMargin + 1) - 1);
+ }
+
+ /**
+ * CNL - Cursor down and to column 1.
+ */
+ private void cnl() {
+ cursorDown(getCsiParam(0, 1, 1, height), true);
+ // To column 0
+ cursorLeft(currentState.cursorX, true);
+ }
+
+ /**
+ * CPL - Cursor up and to column 1.
+ */
+ private void cpl() {
+ cursorUp(getCsiParam(0, 1, 1, currentState.cursorY + 1), true);
+ // To column 0
+ cursorLeft(currentState.cursorX, true);
+ }
+
+ /**
+ * CHA - Cursor to column # in current row.
+ */
+ private void cha() {
+ cursorPosition(currentState.cursorY,
+ getCsiParam(0, 1, 1, rightMargin + 1) - 1);
+ }
+
+ /**
+ * VPA - Cursor to row #, same column.
+ */
+ private void vpa() {
+ cursorPosition(getCsiParam(0, 1, 1, height) - 1,
+ currentState.cursorX);
+ }
+
+ /**
+ * ED - Erase in display.
+ */
+ private void ed() {
+ boolean honorProtected = false;
+ boolean decPrivateModeFlag = false;
+
+ for (int i = 0; i < collectBuffer.length(); i++) {
+ if (collectBuffer.charAt(i) == '?') {
+ decPrivateModeFlag = true;
+ break;
+ }
+ }
+
+ if (((type == DeviceType.VT220) || (type == DeviceType.XTERM))
+ && (decPrivateModeFlag == true)
+ ) {
+ honorProtected = true;
+ }
+
+ int i = getCsiParam(0, 0);
+
+ if (i == 0) {
+ // Erase from here to end of screen
+ if (currentState.cursorY < height - 1) {
+ eraseScreen(currentState.cursorY + 1, 0, height - 1, width - 1,
+ honorProtected);
+ }
+ eraseLine(currentState.cursorX, width - 1, honorProtected);
+ } else if (i == 1) {
+ // Erase from beginning of screen to here
+ eraseScreen(0, 0, currentState.cursorY - 1, width - 1,
+ honorProtected);
+ eraseLine(0, currentState.cursorX, honorProtected);
+ } else if (i == 2) {
+ // Erase entire screen
+ eraseScreen(0, 0, height - 1, width - 1, honorProtected);
+ }
+ }
+
+ /**
+ * EL - Erase in line.
+ */
+ private void el() {
+ boolean honorProtected = false;
+ boolean decPrivateModeFlag = false;
+
+ for (int i = 0; i < collectBuffer.length(); i++) {
+ if (collectBuffer.charAt(i) == '?') {
+ decPrivateModeFlag = true;
+ break;
+ }
+ }
+
+ if (((type == DeviceType.VT220) || (type == DeviceType.XTERM))
+ && (decPrivateModeFlag == true)
+ ) {
+ honorProtected = true;
+ }
+
+ int i = getCsiParam(0, 0);
+
+ if (i == 0) {
+ // Erase from here to end of line
+ eraseLine(currentState.cursorX, width - 1, honorProtected);
+ } else if (i == 1) {
+ // Erase from beginning of line to here
+ eraseLine(0, currentState.cursorX, honorProtected);
+ } else if (i == 2) {
+ // Erase entire line
+ eraseLine(0, width - 1, honorProtected);
+ }
+ }
+
+ /**
+ * ECH - Erase # of characters in current row.
+ */
+ private void ech() {
+ int i = getCsiParam(0, 1, 1, width);
+
+ // Erase from here to i characters
+ eraseLine(currentState.cursorX, currentState.cursorX + i - 1, false);
+ }
+
+ /**
+ * IL - Insert line.
+ */
+ private void il() {
+ int i = getCsiParam(0, 1);
+
+ if ((currentState.cursorY >= scrollRegionTop)
+ && (currentState.cursorY <= scrollRegionBottom)
+ ) {
+
+ // I can get the same effect with a scroll-down
+ scrollingRegionScrollDown(currentState.cursorY,
+ scrollRegionBottom, i);
+ }
+ }
+
+ /**
+ * DCH - Delete char.
+ */
+ private void dch() {
+ int n = getCsiParam(0, 1);
+ DisplayLine line = display.get(currentState.cursorY);
+ Cell blank = new Cell();
+ for (int i = 0; i < n; i++) {
+ line.delete(currentState.cursorX, blank);
+ }
+ }
+
+ /**
+ * ICH - Insert blank char at cursor.
+ */
+ private void ich() {
+ int n = getCsiParam(0, 1);
+ DisplayLine line = display.get(currentState.cursorY);
+ Cell blank = new Cell();
+ for (int i = 0; i < n; i++) {
+ line.insert(currentState.cursorX, blank);
+ }
+ }
+
+ /**
+ * DL - Delete line.
+ */
+ private void dl() {
+ int i = getCsiParam(0, 1);
+
+ if ((currentState.cursorY >= scrollRegionTop)
+ && (currentState.cursorY <= scrollRegionBottom)) {
+
+ // I can get the same effect with a scroll-down
+ scrollingRegionScrollUp(currentState.cursorY,
+ scrollRegionBottom, i);
+ }
+ }
+
+ /**
+ * HVP - Horizontal and vertical position.
+ */
+ private void hvp() {
+ cup();
+ }
+
+ /**
+ * REP - Repeat character.
+ */
+ private void rep() {
+ int n = getCsiParam(0, 1);
+ for (int i = 0; i < n; i++) {
+ printCharacter(repCh);
+ }
+ }
+
+ /**
+ * SU - Scroll up.
+ */
+ private void su() {
+ scrollingRegionScrollUp(scrollRegionTop, scrollRegionBottom,
+ getCsiParam(0, 1, 1, height));
+ }
+
+ /**
+ * SD - Scroll down.
+ */
+ private void sd() {
+ scrollingRegionScrollDown(scrollRegionTop, scrollRegionBottom,
+ getCsiParam(0, 1, 1, height));
+ }
+
+ /**
+ * CBT - Go back X tab stops.
+ */
+ private void cbt() {
+ int tabsToMove = getCsiParam(0, 1);
+ int tabI;
+
+ for (int i = 0; i < tabsToMove; i++) {
+ int j = currentState.cursorX;
+ for (tabI = 0; tabI < tabStops.size(); tabI++) {
+ if (tabStops.get(tabI) >= currentState.cursorX) {
+ break;
+ }
+ }
+ tabI--;
+ if (tabI <= 0) {
+ j = 0;
+ } else {
+ j = tabStops.get(tabI);
+ }
+ cursorPosition(currentState.cursorY, j);
+ }
+ }
+
+ /**
+ * CHT - Advance X tab stops.
+ */
+ private void cht() {
+ int n = getCsiParam(0, 1);
+ for (int i = 0; i < n; i++) {
+ advanceToNextTabStop();
+ }
+ }
+
+ /**
+ * SGR - Select graphics rendition.
+ */
+ private void sgr() {
+
+ if (csiParams.size() == 0) {
+ currentState.attr.reset();
+ return;
+ }
+
+ int sgrColorMode = -1;
+ boolean idx88Color = false;
+ boolean rgbColor = false;
+ int rgbRed = -1;
+ int rgbGreen = -1;
+
+ for (Integer i: csiParams) {
+
+ if ((sgrColorMode == 38) || (sgrColorMode == 48)) {
+
+ assert (type == DeviceType.XTERM);
+
+ if (idx88Color) {
+ /*
+ * Indexed color mode, we now have the index number.
+ */
+ if (sgrColorMode == 38) {
+ currentState.attr.setForeColorRGB(get88Color(i));
+ } else {
+ assert (sgrColorMode == 48);
+ currentState.attr.setBackColorRGB(get88Color(i));
+ }
+ sgrColorMode = -1;
+ idx88Color = false;
+ continue;
+ }
+
+ if (rgbColor) {
+ /*
+ * RGB color mode, we are collecting tokens.
+ */
+ if (rgbRed == -1) {
+ rgbRed = i & 0xFF;
+ } else if (rgbGreen == -1) {
+ rgbGreen = i & 0xFF;
+ } else {
+ int rgb = rgbRed << 16;
+ rgb |= rgbGreen << 8;
+ rgb |= i & 0xFF;
+
+ // System.err.printf("RGB: %08x\n", rgb);
+
+ if (sgrColorMode == 38) {
+ currentState.attr.setForeColorRGB(rgb);
+ } else {
+ assert (sgrColorMode == 48);
+ currentState.attr.setBackColorRGB(rgb);
+ }
+ rgbRed = -1;
+ rgbGreen = -1;
+ sgrColorMode = -1;
+ rgbColor = false;
+ }
+ continue;
+ }
+
+ switch (i) {
+
+ case 2:
+ /*
+ * RGB color mode.
+ */
+ rgbColor = true;
+ break;
+
+ case 5:
+ /*
+ * Indexed color mode.
+ */
+ idx88Color = true;
+ break;
+
+ default:
+ /*
+ * This is neither indexed nor RGB color. Bail out.
+ */
+ return;
+ }
+
+ } // if ((sgrColorMode == 38) || (sgrColorMode == 48))
+
+ switch (i) {
+
+ case 0:
+ // Normal
+ currentState.attr.reset();
+ break;
+
+ case 1:
+ // Bold
+ currentState.attr.setBold(true);
+ break;
+
+ case 4:
+ // Underline
+ currentState.attr.setUnderline(true);
+ break;
+
+ case 5:
+ // Blink
+ currentState.attr.setBlink(true);
+ break;
+
+ case 7:
+ // Reverse
+ currentState.attr.setReverse(true);
+ break;
+
+ default:
+ break;
+ }
+
+ if (type == DeviceType.XTERM) {
+
+ switch (i) {
+
+ case 8:
+ // Invisible
+ // TODO
+ break;
+
+ case 90:
+ // Set black foreground
+ currentState.attr.setForeColorRGB(get88Color(8));
+ break;
+ case 91:
+ // Set red foreground
+ currentState.attr.setForeColorRGB(get88Color(9));
+ break;
+ case 92:
+ // Set green foreground
+ currentState.attr.setForeColorRGB(get88Color(10));
+ break;
+ case 93:
+ // Set yellow foreground
+ currentState.attr.setForeColorRGB(get88Color(11));
+ break;
+ case 94:
+ // Set blue foreground
+ currentState.attr.setForeColorRGB(get88Color(12));
+ break;
+ case 95:
+ // Set magenta foreground
+ currentState.attr.setForeColorRGB(get88Color(13));
+ break;
+ case 96:
+ // Set cyan foreground
+ currentState.attr.setForeColorRGB(get88Color(14));
+ break;
+ case 97:
+ // Set white foreground
+ currentState.attr.setForeColorRGB(get88Color(15));
+ break;
+
+ case 100:
+ // Set black background
+ currentState.attr.setBackColorRGB(get88Color(8));
+ break;
+ case 101:
+ // Set red background
+ currentState.attr.setBackColorRGB(get88Color(9));
+ break;
+ case 102:
+ // Set green background
+ currentState.attr.setBackColorRGB(get88Color(10));
+ break;
+ case 103:
+ // Set yellow background
+ currentState.attr.setBackColorRGB(get88Color(11));
+ break;
+ case 104:
+ // Set blue background
+ currentState.attr.setBackColorRGB(get88Color(12));
+ break;
+ case 105:
+ // Set magenta background
+ currentState.attr.setBackColorRGB(get88Color(13));
+ break;
+ case 106:
+ // Set cyan background
+ currentState.attr.setBackColorRGB(get88Color(14));
+ break;
+ case 107:
+ // Set white background
+ currentState.attr.setBackColorRGB(get88Color(15));
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ if ((type == DeviceType.VT220)
+ || (type == DeviceType.XTERM)) {
+
+ switch (i) {
+
+ case 22:
+ // Normal intensity
+ currentState.attr.setBold(false);
+ break;
+
+ case 24:
+ // No underline
+ currentState.attr.setUnderline(false);
+ break;
+
+ case 25:
+ // No blink
+ currentState.attr.setBlink(false);
+ break;
+
+ case 27:
+ // Un-reverse
+ currentState.attr.setReverse(false);
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ // A true VT100/102/220 does not support color, however everyone
+ // is used to their terminal emulator supporting color so we will
+ // unconditionally support color for all DeviceType's.
+
+ switch (i) {
+
+ case 30:
+ // Set black foreground
+ currentState.attr.setForeColor(Color.BLACK);
+ currentState.attr.setForeColorRGB(-1);
+ break;
+ case 31:
+ // Set red foreground
+ currentState.attr.setForeColor(Color.RED);
+ currentState.attr.setForeColorRGB(-1);
+ break;
+ case 32:
+ // Set green foreground
+ currentState.attr.setForeColor(Color.GREEN);
+ currentState.attr.setForeColorRGB(-1);
+ break;
+ case 33:
+ // Set yellow foreground
+ currentState.attr.setForeColor(Color.YELLOW);
+ currentState.attr.setForeColorRGB(-1);
+ break;
+ case 34:
+ // Set blue foreground
+ currentState.attr.setForeColor(Color.BLUE);
+ currentState.attr.setForeColorRGB(-1);
+ break;
+ case 35:
+ // Set magenta foreground
+ currentState.attr.setForeColor(Color.MAGENTA);
+ currentState.attr.setForeColorRGB(-1);
+ break;
+ case 36:
+ // Set cyan foreground
+ currentState.attr.setForeColor(Color.CYAN);
+ currentState.attr.setForeColorRGB(-1);
+ break;
+ case 37:
+ // Set white foreground
+ currentState.attr.setForeColor(Color.WHITE);
+ currentState.attr.setForeColorRGB(-1);
+ break;
+ case 38:
+ if (type == DeviceType.XTERM) {
+ /*
+ * Xterm supports T.416 / ISO-8613-3 codes to select
+ * either an indexed color or an RGB value. (It also
+ * permits these ISO-8613-3 SGR sequences to be separated
+ * by colons rather than semicolons.)
+ *
+ * We will support only the following:
+ *
+ * 1. Indexed color mode (88- or 256-color modes).
+ *
+ * 2. Direct RGB.
+ *
+ * These cover most of the use cases in the real world.
+ *
+ * HOWEVER, note that this is an awful broken "standard",
+ * with no way to do it "right". See
+ * http://invisible-island.net/ncurses/ncurses.faq.html#xterm_16MegaColors
+ * for a detailed discussion of the current state of RGB
+ * in various terminals, the point of which is that none
+ * of them really do the same thing despite all appearing
+ * to be "xterm".
+ *
+ * Also see
+ * https://bugs.kde.org/show_bug.cgi?id=107487#c3 .
+ * where it is assumed that supporting just the "indexed
+ * mode" of these sequences (which could align easily
+ * with existing SGR colors) is assumed to mean full
+ * support of 24-bit RGB. So it is all or nothing.
+ *
+ * Finally, these sequences break the assumptions of
+ * standard ECMA-48 style parsers as pointed out at
+ * https://bugs.kde.org/show_bug.cgi?id=107487#c11 .
+ * Therefore in order to keep a clean display, we cannot
+ * parse anything else in this sequence.
+ */
+ sgrColorMode = 38;
+ continue;
+ } else {
+ // Underscore on, default foreground color
+ currentState.attr.setUnderline(true);
+ currentState.attr.setForeColor(Color.WHITE);
+ }
+ break;
+ case 39:
+ // Underscore off, default foreground color
+ currentState.attr.setUnderline(false);
+ currentState.attr.setForeColor(Color.WHITE);
+ currentState.attr.setForeColorRGB(-1);
+ break;
+ case 40:
+ // Set black background
+ currentState.attr.setBackColor(Color.BLACK);
+ currentState.attr.setBackColorRGB(-1);
+ break;
+ case 41:
+ // Set red background
+ currentState.attr.setBackColor(Color.RED);
+ currentState.attr.setBackColorRGB(-1);
+ break;
+ case 42:
+ // Set green background
+ currentState.attr.setBackColor(Color.GREEN);
+ currentState.attr.setBackColorRGB(-1);
+ break;
+ case 43:
+ // Set yellow background
+ currentState.attr.setBackColor(Color.YELLOW);
+ currentState.attr.setBackColorRGB(-1);
+ break;
+ case 44:
+ // Set blue background
+ currentState.attr.setBackColor(Color.BLUE);
+ currentState.attr.setBackColorRGB(-1);
+ break;
+ case 45:
+ // Set magenta background
+ currentState.attr.setBackColor(Color.MAGENTA);
+ currentState.attr.setBackColorRGB(-1);
+ break;
+ case 46:
+ // Set cyan background
+ currentState.attr.setBackColor(Color.CYAN);
+ currentState.attr.setBackColorRGB(-1);
+ break;
+ case 47:
+ // Set white background
+ currentState.attr.setBackColor(Color.WHITE);
+ currentState.attr.setBackColorRGB(-1);
+ break;
+ case 48:
+ if (type == DeviceType.XTERM) {
+ /*
+ * Xterm supports T.416 / ISO-8613-3 codes to select
+ * either an indexed color or an RGB value. (It also
+ * permits these ISO-8613-3 SGR sequences to be separated
+ * by colons rather than semicolons.)
+ *
+ * We will support only the following:
+ *
+ * 1. Indexed color mode (88- or 256-color modes).
+ *
+ * 2. Direct RGB.
+ *
+ * These cover most of the use cases in the real world.
+ *
+ * HOWEVER, note that this is an awful broken "standard",
+ * with no way to do it "right". See
+ * http://invisible-island.net/ncurses/ncurses.faq.html#xterm_16MegaColors
+ * for a detailed discussion of the current state of RGB
+ * in various terminals, the point of which is that none
+ * of them really do the same thing despite all appearing
+ * to be "xterm".
+ *
+ * Also see
+ * https://bugs.kde.org/show_bug.cgi?id=107487#c3 .
+ * where it is assumed that supporting just the "indexed
+ * mode" of these sequences (which could align easily
+ * with existing SGR colors) is assumed to mean full
+ * support of 24-bit RGB. So it is all or nothing.
+ *
+ * Finally, these sequences break the assumptions of
+ * standard ECMA-48 style parsers as pointed out at
+ * https://bugs.kde.org/show_bug.cgi?id=107487#c11 .
+ * Therefore in order to keep a clean display, we cannot
+ * parse anything else in this sequence.
+ */
+ sgrColorMode = 48;
+ continue;
+ }
+ break;
+ case 49:
+ // Default background
+ currentState.attr.setBackColor(Color.BLACK);
+ currentState.attr.setBackColorRGB(-1);
+ break;
+
+ default:
+ break;
+ }
+ }
+ }
+
+ /**
+ * DA - Device attributes.
+ */
+ private void da() {
+ int extendedFlag = 0;
+ int i = 0;
+ if (collectBuffer.length() > 0) {
+ String args = collectBuffer.substring(1);
+ if (collectBuffer.charAt(0) == '>') {
+ extendedFlag = 1;
+ if (collectBuffer.length() >= 2) {
+ i = Integer.parseInt(args);
+ }
+ } else if (collectBuffer.charAt(0) == '=') {
+ extendedFlag = 2;
+ if (collectBuffer.length() >= 2) {
+ i = Integer.parseInt(args);
+ }
+ } else {
+ // Unknown code, bail out
+ return;
+ }
+ }
+
+ if ((i != 0) && (i != 1)) {
+ return;
+ }
+
+ if ((extendedFlag == 0) && (i == 0)) {
+ // Send string directly to remote side
+ writeRemote(deviceTypeResponse());
+ return;
+ }
+
+ if ((type == DeviceType.VT220) || (type == DeviceType.XTERM)) {
+
+ if ((extendedFlag == 1) && (i == 0)) {
+ /*
+ * Request "What type of terminal are you, what is your
+ * firmware version, and what hardware options do you have
+ * installed?"
+ *
+ * Respond: "I am a VT220 (identification code of 1), my
+ * firmware version is _____ (Pv), and I have _____ Po
+ * options installed."
+ *
+ * (Same as xterm)
+ *
+ */
+
+ if (s8c1t == true) {
+ writeRemote("\u009b>1;10;0c");
+ } else {
+ writeRemote("\033[>1;10;0c");
+ }
+ }
+ }
+
+ // VT420 and up
+ if ((extendedFlag == 2) && (i == 0)) {
+
+ /*
+ * Request "What is your unit ID?"
+ *
+ * Respond: "I was manufactured at site 00 and have a unique ID
+ * number of 123."
+ *
+ */
+ writeRemote("\033P!|00010203\033\\");
+ }
+ }
+
+ /**
+ * DECSTBM - Set top and bottom margins.
+ */
+ private void decstbm() {
+ boolean decPrivateModeFlag = false;
+
+ for (int i = 0; i < collectBuffer.length(); i++) {
+ if (collectBuffer.charAt(i) == '?') {
+ decPrivateModeFlag = true;
+ break;
+ }
+ }
+ if (decPrivateModeFlag) {
+ // This could be restore DEC private mode values.
+ // Ignore it.
+ } else {
+ // DECSTBM
+ int top = getCsiParam(0, 1, 1, height) - 1;
+ int bottom = getCsiParam(1, height, 1, height) - 1;
+
+ if (top > bottom) {
+ top = bottom;
+ }
+ scrollRegionTop = top;
+ scrollRegionBottom = bottom;
+
+ // Home cursor
+ cursorPosition(0, 0);
+ }
+ }
+
+ /**
+ * DECREQTPARM - Request terminal parameters.
+ */
+ private void decreqtparm() {
+ int i = getCsiParam(0, 0);
+
+ if ((i != 0) && (i != 1)) {
+ return;
+ }
+
+ String str = "";
+
+ /*
+ * Request terminal parameters.
+ *
+ * Respond with:
+ *
+ * Parity NONE, 8 bits, xmitspeed 38400, recvspeed 38400.
+ * (CLoCk MULtiplier = 1, STP option flags = 0)
+ *
+ * (Same as xterm)
+ */
+ if (((type == DeviceType.VT220) || (type == DeviceType.XTERM))
+ && (s8c1t == true)
+ ) {
+ str = String.format("\u009b%d;1;1;128;128;1;0x", i + 2);
+ } else {
+ str = String.format("\033[%d;1;1;128;128;1;0x", i + 2);
+ }
+ writeRemote(str);
+ }
+
+ /**
+ * DECSCA - Select Character Attributes.
+ */
+ private void decsca() {
+ int i = getCsiParam(0, 0);
+
+ if ((i == 0) || (i == 2)) {
+ // Protect mode OFF
+ currentState.attr.setProtect(false);
+ }
+ if (i == 1) {
+ // Protect mode ON
+ currentState.attr.setProtect(true);
+ }
+ }
+
+ /**
+ * DECSTR - Soft Terminal Reset.
+ */
+ private void decstr() {
+ // Do exactly like RIS - Reset to initial state
+ reset();
+ // Do I clear screen too? I think so...
+ eraseScreen(0, 0, height - 1, width - 1, false);
+ cursorPosition(0, 0);
+ }
+
+ /**
+ * DSR - Device status report.
+ */
+ private void dsr() {
+ boolean decPrivateModeFlag = false;
+ int row = currentState.cursorY;
+
+ for (int i = 0; i < collectBuffer.length(); i++) {
+ if (collectBuffer.charAt(i) == '?') {
+ decPrivateModeFlag = true;
+ break;
+ }
+ }
+
+ int i = getCsiParam(0, 0);
+
+ switch (i) {
+
+ case 5:
+ // Request status report. Respond with "OK, no malfunction."
+
+ // Send string directly to remote side
+ if (((type == DeviceType.VT220) || (type == DeviceType.XTERM))
+ && (s8c1t == true)
+ ) {
+ writeRemote("\u009b0n");
+ } else {
+ writeRemote("\033[0n");
+ }
+ break;
+
+ case 6:
+ // Request cursor position. Respond with current position.
+ if (currentState.originMode == true) {
+ row -= scrollRegionTop;
+ }
+ String str = "";
+ if (((type == DeviceType.VT220) || (type == DeviceType.XTERM))
+ && (s8c1t == true)
+ ) {
+ str = String.format("\u009b%d;%dR", row + 1,
+ currentState.cursorX + 1);
+ } else {
+ str = String.format("\033[%d;%dR", row + 1,
+ currentState.cursorX + 1);
+ }
+
+ // Send string directly to remote side
+ writeRemote(str);
+ break;
+
+ case 15:
+ if (decPrivateModeFlag == true) {
+
+ // Request printer status report. Respond with "Printer not
+ // connected."
+
+ if (((type == DeviceType.VT220) || (type == DeviceType.XTERM))
+ && (s8c1t == true)) {
+ writeRemote("\u009b?13n");
+ } else {
+ writeRemote("\033[?13n");
+ }
+ }
+ break;
+
+ case 25:
+ if (((type == DeviceType.VT220) || (type == DeviceType.XTERM))
+ && (decPrivateModeFlag == true)
+ ) {
+
+ // Request user-defined keys are locked or unlocked. Respond
+ // with "User-defined keys are locked."
+
+ if (s8c1t == true) {
+ writeRemote("\u009b?21n");
+ } else {
+ writeRemote("\033[?21n");
+ }
+ }
+ break;
+
+ case 26:
+ if (((type == DeviceType.VT220) || (type == DeviceType.XTERM))
+ && (decPrivateModeFlag == true)
+ ) {
+
+ // Request keyboard language. Respond with "Keyboard
+ // language is North American."
+
+ if (s8c1t == true) {
+ writeRemote("\u009b?27;1n");
+ } else {
+ writeRemote("\033[?27;1n");
+ }
+
+ }
+ break;
+
+ default:
+ // Some other option, ignore
+ break;
+ }
+ }
+
+ /**
+ * TBC - Tabulation clear.
+ */
+ private void tbc() {
+ int i = getCsiParam(0, 0);
+ if (i == 0) {
+ List<Integer> newStops = new ArrayList<Integer>();
+ for (Integer stop: tabStops) {
+ if (stop == currentState.cursorX) {
+ continue;
+ }
+ newStops.add(stop);
+ }
+ tabStops = newStops;
+ }
+ if (i == 3) {
+ tabStops.clear();
+ }
+ }
+
+ /**
+ * Erase the characters in the current line from the start column to the
+ * end column, inclusive.
+ *
+ * @param start starting column to erase (between 0 and width - 1)
+ * @param end ending column to erase (between 0 and width - 1)
+ * @param honorProtected if true, do not erase characters with the
+ * protected attribute set
+ */
+ private void eraseLine(int start, int end, final boolean honorProtected) {
+
+ if (start > end) {
+ return;
+ }
+ if (end > width - 1) {
+ end = width - 1;
+ }
+ if (start < 0) {
+ start = 0;
+ }
+
+ for (int i = start; i <= end; i++) {
+ DisplayLine line = display.get(currentState.cursorY);
+ if ((!honorProtected)
+ || ((honorProtected) && (!line.charAt(i).isProtect()))) {
+
+ switch (type) {
+ case VT100:
+ case VT102:
+ case VT220:
+ /*
+ * From the VT102 manual:
+ *
+ * Erasing a character also erases any character
+ * attribute of the character.
+ */
+ line.setBlank(i);
+ break;
+ case XTERM:
+ /*
+ * Erase with the current color a.k.a. back-color erase
+ * (bce).
+ */
+ line.setChar(i, ' ');
+ line.setAttr(i, currentState.attr);
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Erase a rectangular section of the screen, inclusive. end column,
+ * inclusive.
+ *
+ * @param startRow starting row to erase (between 0 and height - 1)
+ * @param startCol starting column to erase (between 0 and width - 1)
+ * @param endRow ending row to erase (between 0 and height - 1)
+ * @param endCol ending column to erase (between 0 and width - 1)
+ * @param honorProtected if true, do not erase characters with the
+ * protected attribute set
+ */
+ private void eraseScreen(final int startRow, final int startCol,
+ final int endRow, final int endCol, final boolean honorProtected) {
+
+ int oldCursorY;
+
+ if ((startRow < 0)
+ || (startCol < 0)
+ || (endRow < 0)
+ || (endCol < 0)
+ || (endRow < startRow)
+ || (endCol < startCol)
+ ) {
+ return;
+ }
+
+ oldCursorY = currentState.cursorY;
+ for (int i = startRow; i <= endRow; i++) {
+ currentState.cursorY = i;
+ eraseLine(startCol, endCol, honorProtected);
+
+ // Erase display clears the double attributes
+ display.get(i).setDoubleWidth(false);
+ display.get(i).setDoubleHeight(0);
+ }
+ currentState.cursorY = oldCursorY;
+ }
+
+ /**
+ * VT220 printer functions. All of these are parsed, but won't do
+ * anything.
+ */
+ private void printerFunctions() {
+ boolean decPrivateModeFlag = false;
+ for (int i = 0; i < collectBuffer.length(); i++) {
+ if (collectBuffer.charAt(i) == '?') {
+ decPrivateModeFlag = true;
+ break;
+ }
+ }
+
+ int i = getCsiParam(0, 0);
+
+ switch (i) {
+
+ case 0:
+ if (decPrivateModeFlag == false) {
+ // Print screen
+ }
+ break;
+
+ case 1:
+ if (decPrivateModeFlag == true) {
+ // Print cursor line
+ }
+ break;
+
+ case 4:
+ if (decPrivateModeFlag == true) {
+ // Auto print mode OFF
+ } else {
+ // Printer controller OFF
+
+ // Characters re-appear on the screen
+ printerControllerMode = false;
+ }
+ break;
+
+ case 5:
+ if (decPrivateModeFlag == true) {
+ // Auto print mode
+
+ } else {
+ // Printer controller
+
+ // Characters get sucked into oblivion
+ printerControllerMode = true;
+ }
+ break;
+
+ default:
+ break;
+
+ }
+ }
+
+ /**
+ * Handle the SCAN_OSC_STRING state. Handle this in VT100 because lots
+ * of remote systems will send an XTerm title sequence even if TERM isn't
+ * xterm.
+ *
+ * @param xtermChar the character received from the remote side
+ */
+ private void oscPut(final char xtermChar) {
+ // System.err.println("oscPut: " + xtermChar);
+
+ // Collect first
+ collectBuffer.append(xtermChar);
+
+ // Xterm cases...
+ if ((xtermChar == 0x07)
+ || (collectBuffer.toString().endsWith("\033\\"))
+ ) {
+ String args = null;
+ if (xtermChar == 0x07) {
+ args = collectBuffer.substring(0, collectBuffer.length() - 1);
+ } else {
+ args = collectBuffer.substring(0, collectBuffer.length() - 2);
+ }
+
+ String [] p = args.split(";");
+ if (p.length > 0) {
+ if ((p[0].equals("0")) || (p[0].equals("2"))) {
+ if (p.length > 1) {
+ // Screen title
+ screenTitle = p[1];
+ }
+ }
+
+ if (p[0].equals("4")) {
+ for (int i = 1; i + 1 < p.length; i += 2) {
+ // Set a color index value
+ try {
+ set88Color(Integer.parseInt(p[i]), p[i + 1]);
+ } catch (NumberFormatException e) {
+ // SQUASH
+ }
+ }
+ }
+
+ if (p[0].equals("10")) {
+ if (p[1].equals("?")) {
+ // Respond with foreground color.
+ java.awt.Color color = jexer.backend.SwingTerminal.attrToForegroundColor(currentState.attr);
+
+ writeRemote(String.format(
+ "\033]10;rgb:%04x/%04x/%04x\033\\",
+ color.getRed() << 8,
+ color.getGreen() << 8,
+ color.getBlue() << 8));
+ }
+ }
+
+ if (p[0].equals("11")) {
+ if (p[1].equals("?")) {
+ // Respond with background color.
+ java.awt.Color color = jexer.backend.SwingTerminal.attrToBackgroundColor(currentState.attr);
+
+ writeRemote(String.format(
+ "\033]11;rgb:%04x/%04x/%04x\033\\",
+ color.getRed() << 8,
+ color.getGreen() << 8,
+ color.getBlue() << 8));
+ }
+ }
+
+ if (p[0].equals("444") && (p.length == 5)) {
+ // Jexer image
+ parseJexerImage(p[1], p[2], p[3], p[4]);
+ }
+
+ }
+
+ // Go to SCAN_GROUND state
+ toGround();
+ return;
+ }
+ }
+
+ /**
+ * Handle the SCAN_SOSPMAPC_STRING state. This is currently only used by
+ * Jexer ECMA48Terminal to talk to ECMA48.
+ *
+ * @param pmChar the character received from the remote side
+ */
+ private void pmPut(final char pmChar) {
+ // System.err.println("pmPut: " + pmChar);
+
+ // Collect first
+ collectBuffer.append(pmChar);
+
+ // Xterm cases...
+ if (collectBuffer.toString().endsWith("\033\\")) {
+ String arg = null;
+ arg = collectBuffer.substring(0, collectBuffer.length() - 2);
+
+ // System.err.println("arg: '" + arg + "'");
+
+ if (arg.equals("hideMousePointer")) {
+ hideMousePointer = true;
+ }
+ if (arg.equals("showMousePointer")) {
+ hideMousePointer = false;
+ }
+
+ // Go to SCAN_GROUND state
+ toGround();
+ return;
+ }
+ }
+
+ /**
+ * Perform xterm window operations.
+ */
+ private void xtermWindowOps() {
+ boolean xtermPrivateModeFlag = false;
+
+ for (int i = 0; i < collectBuffer.length(); i++) {
+ if (collectBuffer.charAt(i) == '?') {
+ xtermPrivateModeFlag = true;
+ break;
+ }
+ }
+
+ int i = getCsiParam(0, 0);
+
+ if (!xtermPrivateModeFlag) {
+ switch (i) {
+ case 14:
+ // Report xterm text area size in pixels as CSI 4 ; height ;
+ // width t
+ writeRemote(String.format("\033[4;%d;%dt", textHeight * height,
+ textWidth * width));
+ break;
+ case 16:
+ // Report character size in pixels as CSI 6 ; height ; width
+ // t
+ writeRemote(String.format("\033[6;%d;%dt", textHeight,
+ textWidth));
+ break;
+ case 18:
+ // Report the text are size in characters as CSI 8 ; height ;
+ // width t
+ writeRemote(String.format("\033[8;%d;%dt", height, width));
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ /**
+ * Respond to xterm sixel query.
+ */
+ private void xtermSixelQuery() {
+ int item = getCsiParam(0, 0);
+ int action = getCsiParam(1, 0);
+ int value = getCsiParam(2, 0);
+
+ switch (item) {
+ case 1:
+ if (action == 1) {
+ // Report number of color registers.
+ writeRemote(String.format("\033[?%d;%d;%dS", item, 0, 1024));
+ return;
+ }
+ break;
+ default:
+ break;
+ }
+ // We will not support this option.
+ writeRemote(String.format("\033[?%d;%dS", item, action));
+ }
+
+ /**
+ * Run this input character through the ECMA48 state machine.
+ *
+ * @param ch character from the remote side
+ */
+ private void consume(int ch) {
+
+ // 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
+ if ((ch == 0x18) || (ch == 0x1A)) {
+ // CAN and SUB abort escape sequences
+ toGround();
+ return;
+ }
+
+ // 80-8F, 91-97, 99, 9A, 9C --> execute, then switch to SCAN_GROUND
+
+ // 0x1B == ESCAPE
+ if (ch == 0x1B) {
+ if ((type == DeviceType.XTERM)
+ && ((scanState == ScanState.OSC_STRING)
+ || (scanState == ScanState.DCS_SIXEL)
+ || (scanState == ScanState.SOSPMAPC_STRING))
+ ) {
+ // Xterm can pass ESCAPE to its OSC sequence.
+ // Xterm can pass ESCAPE to its DCS sequence.
+ // Jexer can pass ESCAPE to its PM sequence.
+ } else if ((scanState != ScanState.DCS_ENTRY)
+ && (scanState != ScanState.DCS_INTERMEDIATE)
+ && (scanState != ScanState.DCS_IGNORE)
+ && (scanState != ScanState.DCS_PARAM)
+ && (scanState != ScanState.DCS_PASSTHROUGH)
+ ) {
+ scanState = ScanState.ESCAPE;
+ return;
+ }
+ }
+
+ // 0x9B == CSI 8-bit sequence
+ if (ch == 0x9B) {
+ scanState = ScanState.CSI_ENTRY;
+ return;
+ }
+
+ // 0x9D goes to ScanState.OSC_STRING
+ if (ch == 0x9D) {
+ scanState = ScanState.OSC_STRING;
+ return;
+ }
+
+ // 0x90 goes to DCS_ENTRY
+ if (ch == 0x90) {
+ scanState = ScanState.DCS_ENTRY;
+ return;
+ }
+
+ // 0x98, 0x9E, and 0x9F go to SOSPMAPC_STRING
+ if ((ch == 0x98) || (ch == 0x9E) || (ch == 0x9F)) {
+ scanState = ScanState.SOSPMAPC_STRING;
+ return;
+ }
+
+ // 0x7F (DEL) is always discarded
+ if (ch == 0x7F) {
+ return;
+ }
+
+ switch (scanState) {
+
+ case GROUND:
+ // 00-17, 19, 1C-1F --> execute
+ // 80-8F, 91-9A, 9C --> execute
+ if ((ch <= 0x1F) || ((ch >= 0x80) && (ch <= 0x9F))) {
+ handleControlChar((char) ch);
+ }
+
+ // 20-7F --> print
+ if (((ch >= 0x20) && (ch <= 0x7F))
+ || (ch >= 0xA0)
+ ) {
+
+ // VT220 printer --> trash bin
+ if (((type == DeviceType.VT220)
+ || (type == DeviceType.XTERM))
+ && (printerControllerMode == true)
+ ) {
+ return;
+ }
+
+ // Hang onto this character
+ repCh = mapCharacter(ch);
+
+ // Print this character
+ printCharacter(repCh);
+ }
+ return;
+
+ case ESCAPE:
+ // 00-17, 19, 1C-1F --> execute
+ if (ch <= 0x1F) {
+ handleControlChar((char) ch);
+ return;
+ }
+
+ // 20-2F --> collect, then switch to ESCAPE_INTERMEDIATE
+ if ((ch >= 0x20) && (ch <= 0x2F)) {
+ collect((char) ch);
+ scanState = ScanState.ESCAPE_INTERMEDIATE;
+ return;
+ }
+
+ // 30-4F, 51-57, 59, 5A, 5C, 60-7E --> dispatch, then switch to GROUND
+ if ((ch >= 0x30) && (ch <= 0x4F)) {
+ switch (ch) {
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ break;
+ case '7':
+ // DECSC - Save cursor
+ // Note this code overlaps both ANSI and VT52 mode
+ decsc();
+ break;
+
+ case '8':
+ // DECRC - Restore cursor
+ // Note this code overlaps both ANSI and VT52 mode
+ decrc();
+ break;
+
+ case '9':
+ case ':':
+ case ';':
+ break;
+ case '<':
+ if (vt52Mode == true) {
+ // DECANM - Enter ANSI mode
+ vt52Mode = false;
+ arrowKeyMode = ArrowKeyMode.VT100;
+
+ /*
+ * From the VT102 docs: "You use ANSI mode to select
+ * most terminal features; the terminal uses the same
+ * features when it switches to VT52 mode. You
+ * cannot, however, change most of these features in
+ * VT52 mode."
+ *
+ * In other words, do not reset any other attributes
+ * when switching between VT52 submode and ANSI.
+ */
+
+ // Reset fonts
+ currentState.g0Charset = CharacterSet.US;
+ currentState.g1Charset = CharacterSet.DRAWING;
+ s8c1t = false;
+ singleshift = Singleshift.NONE;
+ currentState.glLockshift = LockshiftMode.NONE;
+ currentState.grLockshift = LockshiftMode.NONE;
+ }
+ break;
+ case '=':
+ // DECKPAM - Keypad application mode
+ // Note this code overlaps both ANSI and VT52 mode
+ deckpam();
+ break;
+ case '>':
+ // DECKPNM - Keypad numeric mode
+ // Note this code overlaps both ANSI and VT52 mode
+ deckpnm();
+ break;
+ case '?':
+ case '@':
+ break;
+ case 'A':
+ if (vt52Mode == true) {
+ // Cursor up, and stop at the top without scrolling
+ cursorUp(1, false);
+ }
+ break;
+ case 'B':
+ if (vt52Mode == true) {
+ // Cursor down, and stop at the bottom without scrolling
+ cursorDown(1, false);
+ }
+ break;
+ case 'C':
+ if (vt52Mode == true) {
+ // Cursor right, and stop at the right without scrolling
+ cursorRight(1, false);
+ }
+ break;
+ case 'D':
+ if (vt52Mode == true) {
+ // Cursor left, and stop at the left without scrolling
+ cursorLeft(1, false);
+ } else {
+ // IND - Index
+ ind();
+ }
+ break;
+ case 'E':
+ if (vt52Mode == true) {
+ // Nothing
+ } else {
+ // NEL - Next line
+ nel();
+ }
+ break;
+ case 'F':
+ if (vt52Mode == true) {
+ // G0 --> Special graphics
+ currentState.g0Charset = CharacterSet.VT52_GRAPHICS;
+ }
+ break;
+ case 'G':
+ if (vt52Mode == true) {
+ // G0 --> ASCII set
+ currentState.g0Charset = CharacterSet.US;
+ }
+ break;
+ case 'H':
+ if (vt52Mode == true) {
+ // Cursor to home
+ cursorPosition(0, 0);
+ } else {
+ // HTS - Horizontal tabulation set
+ hts();
+ }
+ break;
+ case 'I':
+ if (vt52Mode == true) {
+ // Reverse line feed. Same as RI.
+ ri();
+ }
+ break;
+ case 'J':
+ if (vt52Mode == true) {
+ // Erase to end of screen
+ eraseLine(currentState.cursorX, width - 1, false);
+ eraseScreen(currentState.cursorY + 1, 0, height - 1,
+ width - 1, false);
+ }
+ break;
+ case 'K':
+ if (vt52Mode == true) {
+ // Erase to end of line
+ eraseLine(currentState.cursorX, width - 1, false);
+ }
+ break;
+ case 'L':
+ break;
+ case 'M':
+ if (vt52Mode == true) {
+ // Nothing
+ } else {
+ // RI - Reverse index
+ ri();
+ }
+ break;
+ case 'N':
+ if (vt52Mode == false) {
+ // SS2
+ singleshift = Singleshift.SS2;
+ }
+ break;
+ case 'O':
+ if (vt52Mode == false) {
+ // SS3
+ singleshift = Singleshift.SS3;
+ }
+ break;
+ }
+ toGround();
+ return;
+ }
+ if ((ch >= 0x51) && (ch <= 0x57)) {
+ switch (ch) {
+ case 'Q':
+ case 'R':
+ case 'S':
+ case 'T':
+ case 'U':
+ case 'V':
+ case 'W':
+ break;
+ }
+ toGround();
+ return;
+ }
+ if (ch == 0x59) {
+ // 'Y'
+ if (vt52Mode == true) {
+ scanState = ScanState.VT52_DIRECT_CURSOR_ADDRESS;
+ } else {
+ toGround();
+ }
+ return;
+ }
+ if (ch == 0x5A) {
+ // 'Z'
+ if (vt52Mode == true) {
+ // Identify
+ // Send string directly to remote side
+ writeRemote("\033/Z");
+ } else {
+ // DECID
+ // Send string directly to remote side
+ writeRemote(deviceTypeResponse());
+ }
+ toGround();
+ return;
+ }
+ if (ch == 0x5C) {
+ // '\'
+ toGround();
+ return;
+ }
+
+ // VT52 cannot get to any of these other states
+ if (vt52Mode == true) {
+ toGround();
+ return;
+ }
+
+ if ((ch >= 0x60) && (ch <= 0x7E)) {
+ switch (ch) {
+ case '`':
+ case 'a':
+ case 'b':
+ break;
+ case 'c':
+ // RIS - Reset to initial state
+ reset();
+ // Do I clear screen too? I think so...
+ eraseScreen(0, 0, height - 1, width - 1, false);
+ cursorPosition(0, 0);
+ break;
+ case 'd':
+ case 'e':
+ case 'f':
+ case 'g':
+ case 'h':
+ case 'i':
+ case 'j':
+ case 'k':
+ case 'l':
+ case 'm':
+ break;
+ case 'n':
+ if ((type == DeviceType.VT220)
+ || (type == DeviceType.XTERM)) {
+
+ // VT220 lockshift G2 into GL
+ currentState.glLockshift = LockshiftMode.G2_GL;
+ shiftOut = false;
+ }
+ break;
+ case 'o':
+ if ((type == DeviceType.VT220)
+ || (type == DeviceType.XTERM)) {
+
+ // VT220 lockshift G3 into GL
+ currentState.glLockshift = LockshiftMode.G3_GL;
+ shiftOut = false;
+ }
+ break;
+ case 'p':
+ case 'q':
+ case 'r':
+ case 's':
+ case 't':
+ case 'u':
+ case 'v':
+ case 'w':
+ case 'x':
+ case 'y':
+ case 'z':
+ case '{':
+ break;
+ case '|':
+ if ((type == DeviceType.VT220)
+ || (type == DeviceType.XTERM)) {
+
+ // VT220 lockshift G3 into GR
+ currentState.grLockshift = LockshiftMode.G3_GR;
+ shiftOut = false;
+ }
+ break;
+ case '}':
+ if ((type == DeviceType.VT220)
+ || (type == DeviceType.XTERM)) {
+
+ // VT220 lockshift G2 into GR
+ currentState.grLockshift = LockshiftMode.G2_GR;
+ shiftOut = false;
+ }
+ break;
+
+ case '~':
+ if ((type == DeviceType.VT220)
+ || (type == DeviceType.XTERM)) {
+
+ // VT220 lockshift G1 into GR
+ currentState.grLockshift = LockshiftMode.G1_GR;
+ shiftOut = false;
+ }
+ break;
+ }
+ toGround();
+ }
+
+ // 7F --> ignore
+
+ // 0x5B goes to CSI_ENTRY
+ if (ch == 0x5B) {
+ scanState = ScanState.CSI_ENTRY;
+ }
+
+ // 0x5D goes to OSC_STRING
+ if (ch == 0x5D) {
+ scanState = ScanState.OSC_STRING;
+ }
+
+ // 0x50 goes to DCS_ENTRY
+ if (ch == 0x50) {
+ scanState = ScanState.DCS_ENTRY;
+ }
+
+ // 0x58, 0x5E, and 0x5F go to SOSPMAPC_STRING
+ if ((ch == 0x58) || (ch == 0x5E) || (ch == 0x5F)) {
+ scanState = ScanState.SOSPMAPC_STRING;
+ }
+
+ return;
+
+ case ESCAPE_INTERMEDIATE:
+ // 00-17, 19, 1C-1F --> execute
+ if (ch <= 0x1F) {
+ handleControlChar((char) ch);
+ }
+
+ // 20-2F --> collect
+ if ((ch >= 0x20) && (ch <= 0x2F)) {
+ collect((char) ch);
+ }
+
+ // 30-7E --> dispatch, then switch to GROUND
+ if ((ch >= 0x30) && (ch <= 0x7E)) {
+ switch (ch) {
+ case '0':
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '(')) {
+ // G0 --> Special graphics
+ currentState.g0Charset = CharacterSet.DRAWING;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == ')')) {
+ // G1 --> Special graphics
+ currentState.g1Charset = CharacterSet.DRAWING;
+ }
+ if ((type == DeviceType.VT220)
+ || (type == DeviceType.XTERM)) {
+
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '*')) {
+ // G2 --> Special graphics
+ currentState.g2Charset = CharacterSet.DRAWING;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '+')) {
+ // G3 --> Special graphics
+ currentState.g3Charset = CharacterSet.DRAWING;
+ }
+ }
+ break;
+ case '1':
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '(')) {
+ // G0 --> Alternate character ROM standard character set
+ currentState.g0Charset = CharacterSet.ROM;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == ')')) {
+ // G1 --> Alternate character ROM standard character set
+ currentState.g1Charset = CharacterSet.ROM;
+ }
+ break;
+ case '2':
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '(')) {
+ // G0 --> Alternate character ROM special graphics
+ currentState.g0Charset = CharacterSet.ROM_SPECIAL;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == ')')) {
+ // G1 --> Alternate character ROM special graphics
+ currentState.g1Charset = CharacterSet.ROM_SPECIAL;
+ }
+ break;
+ case '3':
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '#')) {
+ // DECDHL - Double-height line (top half)
+ dechdl(true);
+ }
+ break;
+ case '4':
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '#')) {
+ // DECDHL - Double-height line (bottom half)
+ dechdl(false);
+ }
+ if ((type == DeviceType.VT220)
+ || (type == DeviceType.XTERM)) {
+
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '(')) {
+ // G0 --> DUTCH
+ currentState.g0Charset = CharacterSet.NRC_DUTCH;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == ')')) {
+ // G1 --> DUTCH
+ currentState.g1Charset = CharacterSet.NRC_DUTCH;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '*')) {
+ // G2 --> DUTCH
+ currentState.g2Charset = CharacterSet.NRC_DUTCH;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '+')) {
+ // G3 --> DUTCH
+ currentState.g3Charset = CharacterSet.NRC_DUTCH;
+ }
+ }
+ break;
+ case '5':
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '#')) {
+ // DECSWL - Single-width line
+ decswl();
+ }
+ if ((type == DeviceType.VT220)
+ || (type == DeviceType.XTERM)) {
+
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '(')) {
+ // G0 --> FINNISH
+ currentState.g0Charset = CharacterSet.NRC_FINNISH;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == ')')) {
+ // G1 --> FINNISH
+ currentState.g1Charset = CharacterSet.NRC_FINNISH;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '*')) {
+ // G2 --> FINNISH
+ currentState.g2Charset = CharacterSet.NRC_FINNISH;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '+')) {
+ // G3 --> FINNISH
+ currentState.g3Charset = CharacterSet.NRC_FINNISH;
+ }
+ }
+ break;
+ case '6':
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '#')) {
+ // DECDWL - Double-width line
+ decdwl();
+ }
+ if ((type == DeviceType.VT220)
+ || (type == DeviceType.XTERM)) {
+
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '(')) {
+ // G0 --> NORWEGIAN
+ currentState.g0Charset = CharacterSet.NRC_NORWEGIAN;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == ')')) {
+ // G1 --> NORWEGIAN
+ currentState.g1Charset = CharacterSet.NRC_NORWEGIAN;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '*')) {
+ // G2 --> NORWEGIAN
+ currentState.g2Charset = CharacterSet.NRC_NORWEGIAN;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '+')) {
+ // G3 --> NORWEGIAN
+ currentState.g3Charset = CharacterSet.NRC_NORWEGIAN;
+ }
+ }
+ break;
+ case '7':
+ if ((type == DeviceType.VT220)
+ || (type == DeviceType.XTERM)) {
+
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '(')) {
+ // G0 --> SWEDISH
+ currentState.g0Charset = CharacterSet.NRC_SWEDISH;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == ')')) {
+ // G1 --> SWEDISH
+ currentState.g1Charset = CharacterSet.NRC_SWEDISH;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '*')) {
+ // G2 --> SWEDISH
+ currentState.g2Charset = CharacterSet.NRC_SWEDISH;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '+')) {
+ // G3 --> SWEDISH
+ currentState.g3Charset = CharacterSet.NRC_SWEDISH;
+ }
+ }
+ break;
+ case '8':
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '#')) {
+ // DECALN - Screen alignment display
+ decaln();
+ }
+ break;
+ case '9':
+ case ':':
+ case ';':
+ break;
+ case '<':
+ if ((type == DeviceType.VT220)
+ || (type == DeviceType.XTERM)) {
+
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '(')) {
+ // G0 --> DEC_SUPPLEMENTAL
+ currentState.g0Charset = CharacterSet.DEC_SUPPLEMENTAL;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == ')')) {
+ // G1 --> DEC_SUPPLEMENTAL
+ currentState.g1Charset = CharacterSet.DEC_SUPPLEMENTAL;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '*')) {
+ // G2 --> DEC_SUPPLEMENTAL
+ currentState.g2Charset = CharacterSet.DEC_SUPPLEMENTAL;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '+')) {
+ // G3 --> DEC_SUPPLEMENTAL
+ currentState.g3Charset = CharacterSet.DEC_SUPPLEMENTAL;
+ }
+ }
+ break;
+ case '=':
+ if ((type == DeviceType.VT220)
+ || (type == DeviceType.XTERM)) {
+
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '(')) {
+ // G0 --> SWISS
+ currentState.g0Charset = CharacterSet.NRC_SWISS;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == ')')) {
+ // G1 --> SWISS
+ currentState.g1Charset = CharacterSet.NRC_SWISS;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '*')) {
+ // G2 --> SWISS
+ currentState.g2Charset = CharacterSet.NRC_SWISS;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '+')) {
+ // G3 --> SWISS
+ currentState.g3Charset = CharacterSet.NRC_SWISS;
+ }
+ }
+ break;
+ case '>':
+ case '?':
+ case '@':
+ break;
+ case 'A':
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '(')) {
+ // G0 --> United Kingdom set
+ currentState.g0Charset = CharacterSet.UK;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == ')')) {
+ // G1 --> United Kingdom set
+ currentState.g1Charset = CharacterSet.UK;
+ }
+ if ((type == DeviceType.VT220)
+ || (type == DeviceType.XTERM)) {
+
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '*')) {
+ // G2 --> United Kingdom set
+ currentState.g2Charset = CharacterSet.UK;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '+')) {
+ // G3 --> United Kingdom set
+ currentState.g3Charset = CharacterSet.UK;
+ }
+ }
+ break;
+ case 'B':
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '(')) {
+ // G0 --> ASCII set
+ currentState.g0Charset = CharacterSet.US;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == ')')) {
+ // G1 --> ASCII set
+ currentState.g1Charset = CharacterSet.US;
+ }
+ if ((type == DeviceType.VT220)
+ || (type == DeviceType.XTERM)) {
+
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '*')) {
+ // G2 --> ASCII
+ currentState.g2Charset = CharacterSet.US;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '+')) {
+ // G3 --> ASCII
+ currentState.g3Charset = CharacterSet.US;
+ }
+ }
+ break;
+ case 'C':
+ if ((type == DeviceType.VT220)
+ || (type == DeviceType.XTERM)) {
+
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '(')) {
+ // G0 --> FINNISH
+ currentState.g0Charset = CharacterSet.NRC_FINNISH;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == ')')) {
+ // G1 --> FINNISH
+ currentState.g1Charset = CharacterSet.NRC_FINNISH;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '*')) {
+ // G2 --> FINNISH
+ currentState.g2Charset = CharacterSet.NRC_FINNISH;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '+')) {
+ // G3 --> FINNISH
+ currentState.g3Charset = CharacterSet.NRC_FINNISH;
+ }
+ }
+ break;
+ case 'D':
+ break;
+ case 'E':
+ if ((type == DeviceType.VT220)
+ || (type == DeviceType.XTERM)) {
+
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '(')) {
+ // G0 --> NORWEGIAN
+ currentState.g0Charset = CharacterSet.NRC_NORWEGIAN;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == ')')) {
+ // G1 --> NORWEGIAN
+ currentState.g1Charset = CharacterSet.NRC_NORWEGIAN;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '*')) {
+ // G2 --> NORWEGIAN
+ currentState.g2Charset = CharacterSet.NRC_NORWEGIAN;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '+')) {
+ // G3 --> NORWEGIAN
+ currentState.g3Charset = CharacterSet.NRC_NORWEGIAN;
+ }
+ }
+ break;
+ case 'F':
+ if ((type == DeviceType.VT220)
+ || (type == DeviceType.XTERM)) {
+
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == ' ')) {
+ // S7C1T
+ s8c1t = false;
+ }
+ }
+ break;
+ case 'G':
+ if ((type == DeviceType.VT220)
+ || (type == DeviceType.XTERM)) {
+
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == ' ')) {
+ // S8C1T
+ s8c1t = true;
+ }
+ }
+ break;
+ case 'H':
+ if ((type == DeviceType.VT220)
+ || (type == DeviceType.XTERM)) {
+
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '(')) {
+ // G0 --> SWEDISH
+ currentState.g0Charset = CharacterSet.NRC_SWEDISH;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == ')')) {
+ // G1 --> SWEDISH
+ currentState.g1Charset = CharacterSet.NRC_SWEDISH;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '*')) {
+ // G2 --> SWEDISH
+ currentState.g2Charset = CharacterSet.NRC_SWEDISH;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '+')) {
+ // G3 --> SWEDISH
+ currentState.g3Charset = CharacterSet.NRC_SWEDISH;
+ }
+ }
+ break;
+ case 'I':
+ case 'J':
+ break;
+ case 'K':
+ if ((type == DeviceType.VT220)
+ || (type == DeviceType.XTERM)) {
+
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '(')) {
+ // G0 --> GERMAN
+ currentState.g0Charset = CharacterSet.NRC_GERMAN;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == ')')) {
+ // G1 --> GERMAN
+ currentState.g1Charset = CharacterSet.NRC_GERMAN;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '*')) {
+ // G2 --> GERMAN
+ currentState.g2Charset = CharacterSet.NRC_GERMAN;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '+')) {
+ // G3 --> GERMAN
+ currentState.g3Charset = CharacterSet.NRC_GERMAN;
+ }
+ }
+ break;
+ case 'L':
+ case 'M':
+ case 'N':
+ case 'O':
+ case 'P':
+ break;
+ case 'Q':
+ if ((type == DeviceType.VT220)
+ || (type == DeviceType.XTERM)) {
+
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '(')) {
+ // G0 --> FRENCH_CA
+ currentState.g0Charset = CharacterSet.NRC_FRENCH_CA;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == ')')) {
+ // G1 --> FRENCH_CA
+ currentState.g1Charset = CharacterSet.NRC_FRENCH_CA;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '*')) {
+ // G2 --> FRENCH_CA
+ currentState.g2Charset = CharacterSet.NRC_FRENCH_CA;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '+')) {
+ // G3 --> FRENCH_CA
+ currentState.g3Charset = CharacterSet.NRC_FRENCH_CA;
+ }
+ }
+ break;
+ case 'R':
+ if ((type == DeviceType.VT220)
+ || (type == DeviceType.XTERM)) {
+
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '(')) {
+ // G0 --> FRENCH
+ currentState.g0Charset = CharacterSet.NRC_FRENCH;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == ')')) {
+ // G1 --> FRENCH
+ currentState.g1Charset = CharacterSet.NRC_FRENCH;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '*')) {
+ // G2 --> FRENCH
+ currentState.g2Charset = CharacterSet.NRC_FRENCH;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '+')) {
+ // G3 --> FRENCH
+ currentState.g3Charset = CharacterSet.NRC_FRENCH;
+ }
+ }
+ break;
+ case 'S':
+ case 'T':
+ case 'U':
+ case 'V':
+ case 'W':
+ case 'X':
+ break;
+ case 'Y':
+ if ((type == DeviceType.VT220)
+ || (type == DeviceType.XTERM)) {
+
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '(')) {
+ // G0 --> ITALIAN
+ currentState.g0Charset = CharacterSet.NRC_ITALIAN;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == ')')) {
+ // G1 --> ITALIAN
+ currentState.g1Charset = CharacterSet.NRC_ITALIAN;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '*')) {
+ // G2 --> ITALIAN
+ currentState.g2Charset = CharacterSet.NRC_ITALIAN;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '+')) {
+ // G3 --> ITALIAN
+ currentState.g3Charset = CharacterSet.NRC_ITALIAN;
+ }
+ }
+ break;
+ case 'Z':
+ if ((type == DeviceType.VT220)
+ || (type == DeviceType.XTERM)) {
+
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '(')) {
+ // G0 --> SPANISH
+ currentState.g0Charset = CharacterSet.NRC_SPANISH;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == ')')) {
+ // G1 --> SPANISH
+ currentState.g1Charset = CharacterSet.NRC_SPANISH;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '*')) {
+ // G2 --> SPANISH
+ currentState.g2Charset = CharacterSet.NRC_SPANISH;
+ }
+ if ((collectBuffer.length() == 1)
+ && (collectBuffer.charAt(0) == '+')) {
+ // G3 --> SPANISH
+ currentState.g3Charset = CharacterSet.NRC_SPANISH;
+ }
+ }
+ break;
+ case '[':
+ case '\\':
+ case ']':
+ case '^':
+ case '_':
+ case '`':
+ case 'a':
+ case 'b':
+ case 'c':
+ case 'd':
+ case 'e':
+ case 'f':
+ case 'g':
+ case 'h':
+ case 'i':
+ case 'j':
+ case 'k':
+ case 'l':
+ case 'm':
+ case 'n':
+ case 'o':
+ case 'p':
+ case 'q':
+ case 'r':
+ case 's':
+ case 't':
+ case 'u':
+ case 'v':
+ case 'w':
+ case 'x':
+ case 'y':
+ case 'z':
+ case '{':
+ case '|':
+ case '}':
+ case '~':
+ break;
+ }
+ toGround();
+ }
+
+ // 7F --> ignore
+
+ // 0x9C goes to GROUND
+ if (ch == 0x9C) {
+ toGround();
+ }
+
+ return;
+
+ case CSI_ENTRY:
+ // 00-17, 19, 1C-1F --> execute
+ if (ch <= 0x1F) {
+ handleControlChar((char) ch);
+ }
+
+ // 20-2F --> collect, then switch to CSI_INTERMEDIATE
+ if ((ch >= 0x20) && (ch <= 0x2F)) {
+ collect((char) ch);
+ scanState = ScanState.CSI_INTERMEDIATE;
+ }
+
+ // 30-39, 3B --> param, then switch to CSI_PARAM
+ if ((ch >= '0') && (ch <= '9')) {
+ param((byte) ch);
+ scanState = ScanState.CSI_PARAM;
+ }
+ if (ch == ';') {
+ param((byte) ch);
+ scanState = ScanState.CSI_PARAM;
+ }
+
+ // 3C-3F --> collect, then switch to CSI_PARAM
+ if ((ch >= 0x3C) && (ch <= 0x3F)) {
+ collect((char) ch);
+ scanState = ScanState.CSI_PARAM;
+ }
+
+ // 40-7E --> dispatch, then switch to GROUND
+ if ((ch >= 0x40) && (ch <= 0x7E)) {
+ switch (ch) {
+ case '@':
+ // ICH - Insert character
+ ich();
+ break;
+ case 'A':
+ // CUU - Cursor up
+ cuu();
+ break;
+ case 'B':
+ // CUD - Cursor down
+ cud();
+ break;
+ case 'C':
+ // CUF - Cursor forward
+ cuf();
+ break;
+ case 'D':
+ // CUB - Cursor backward
+ cub();
+ break;
+ case 'E':
+ // CNL - Cursor down and to column 1
+ if (type == DeviceType.XTERM) {
+ cnl();
+ }
+ break;
+ case 'F':
+ // CPL - Cursor up and to column 1
+ if (type == DeviceType.XTERM) {
+ cpl();
+ }
+ break;
+ case 'G':
+ // CHA - Cursor to column # in current row
+ if (type == DeviceType.XTERM) {
+ cha();
+ }
+ break;
+ case 'H':
+ // CUP - Cursor position
+ cup();
+ break;
+ case 'I':
+ // CHT - Cursor forward X tab stops (default 1)
+ if (type == DeviceType.XTERM) {
+ cht();
+ }
+ break;
+ case 'J':
+ // ED - Erase in display
+ ed();
+ break;
+ case 'K':
+ // EL - Erase in line
+ el();
+ break;
+ case 'L':
+ // IL - Insert line
+ il();
+ break;
+ case 'M':
+ // DL - Delete line
+ dl();
+ break;
+ case 'N':
+ case 'O':
+ break;
+ case 'P':
+ // DCH - Delete character
+ dch();
+ break;
+ case 'Q':
+ case 'R':
+ break;
+ case 'S':
+ // Scroll up X lines (default 1)
+ if (type == DeviceType.XTERM) {
+ boolean xtermPrivateModeFlag = false;
+ for (int i = 0; i < collectBuffer.length(); i++) {
+ if (collectBuffer.charAt(i) == '?') {
+ xtermPrivateModeFlag = true;
+ break;
+ }
+ }
+ if (xtermPrivateModeFlag) {
+ xtermSixelQuery();
+ } else {
+ su();
+ }
+ }
+ break;
+ case 'T':
+ // Scroll down X lines (default 1)
+ if (type == DeviceType.XTERM) {
+ sd();
+ }
+ break;
+ case 'U':
+ case 'V':
+ case 'W':
+ break;
+ case 'X':
+ if ((type == DeviceType.VT220)
+ || (type == DeviceType.XTERM)) {
+
+ // ECH - Erase character
+ ech();
+ }
+ break;
+ case 'Y':
+ break;
+ case 'Z':
+ // CBT - Cursor backward X tab stops (default 1)
+ if (type == DeviceType.XTERM) {
+ cbt();
+ }
+ break;
+ case '[':
+ case '\\':
+ case ']':
+ case '^':
+ case '_':
+ break;
+ case '`':
+ // HPA - Cursor to column # in current row. Same as CHA
+ if (type == DeviceType.XTERM) {
+ cha();
+ }
+ break;
+ case 'a':
+ // HPR - Cursor right. Same as CUF
+ if (type == DeviceType.XTERM) {
+ cuf();
+ }
+ break;
+ case 'b':
+ // REP - Repeat last char X times
+ if (type == DeviceType.XTERM) {
+ rep();
+ }
+ break;
+ case 'c':
+ // DA - Device attributes
+ da();
+ break;
+ case 'd':
+ // VPA - Cursor to row, current column.
+ if (type == DeviceType.XTERM) {
+ vpa();
+ }
+ break;
+ case 'e':
+ // VPR - Cursor down. Same as CUD
+ if (type == DeviceType.XTERM) {
+ cud();
+ }
+ break;
+ case 'f':
+ // HVP - Horizontal and vertical position
+ hvp();
+ break;
+ case 'g':
+ // TBC - Tabulation clear
+ tbc();
+ break;
+ case 'h':
+ // Sets an ANSI or DEC private toggle
+ setToggle(true);
+ break;
+ case 'i':
+ if ((type == DeviceType.VT220)
+ || (type == DeviceType.XTERM)) {
+
+ // Printer functions
+ printerFunctions();
+ }
+ break;
+ case 'j':
+ case 'k':
+ break;
+ case 'l':
+ // Sets an ANSI or DEC private toggle
+ setToggle(false);
+ break;
+ case 'm':
+ // SGR - Select graphics rendition
+ sgr();
+ break;
+ case 'n':
+ // DSR - Device status report
+ dsr();
+ break;
+ case 'o':
+ case 'p':
+ break;
+ case 'q':
+ // DECLL - Load leds
+ // Not supported
+ break;
+ case 'r':
+ // DECSTBM - Set top and bottom margins
+ decstbm();
+ break;
+ case 's':
+ // Save cursor (ANSI.SYS)
+ if (type == DeviceType.XTERM) {
+ savedState.cursorX = currentState.cursorX;
+ savedState.cursorY = currentState.cursorY;
+ }
+ break;
+ case 't':
+ if (type == DeviceType.XTERM) {
+ // Window operations
+ xtermWindowOps();
+ }
+ break;
+ case 'u':
+ // Restore cursor (ANSI.SYS)
+ if (type == DeviceType.XTERM) {
+ cursorPosition(savedState.cursorY, savedState.cursorX);
+ }
+ break;
+ case 'v':
+ case 'w':
+ break;
+ case 'x':
+ // DECREQTPARM - Request terminal parameters
+ decreqtparm();
+ break;
+ case 'y':
+ case 'z':
+ case '{':
+ case '|':
+ case '}':
+ case '~':
+ break;
+ }
+ toGround();
+ }
+
+ // 7F --> ignore
+
+ // 0x9C goes to GROUND
+ if (ch == 0x9C) {
+ toGround();
+ }
+
+ // 0x3A goes to CSI_IGNORE
+ if (ch == 0x3A) {
+ scanState = ScanState.CSI_IGNORE;
+ }
+ return;
+
+ case CSI_PARAM:
+ // 00-17, 19, 1C-1F --> execute
+ if (ch <= 0x1F) {
+ handleControlChar((char) ch);
+ }
+
+ // 20-2F --> collect, then switch to CSI_INTERMEDIATE
+ if ((ch >= 0x20) && (ch <= 0x2F)) {
+ collect((char) ch);
+ scanState = ScanState.CSI_INTERMEDIATE;
+ }
+
+ // 30-39, 3B --> param
+ if ((ch >= '0') && (ch <= '9')) {
+ param((byte) ch);
+ }
+ if (ch == ';') {
+ param((byte) ch);
+ }
+
+ // 0x3A goes to CSI_IGNORE
+ if (ch == 0x3A) {
+ scanState = ScanState.CSI_IGNORE;
+ }
+ // 0x3C-3F goes to CSI_IGNORE
+ if ((ch >= 0x3C) && (ch <= 0x3F)) {
+ scanState = ScanState.CSI_IGNORE;
+ }
+
+ // 40-7E --> dispatch, then switch to GROUND
+ if ((ch >= 0x40) && (ch <= 0x7E)) {
+ switch (ch) {
+ case '@':
+ // ICH - Insert character
+ ich();
+ break;
+ case 'A':
+ // CUU - Cursor up
+ cuu();
+ break;
+ case 'B':
+ // CUD - Cursor down
+ cud();
+ break;
+ case 'C':
+ // CUF - Cursor forward
+ cuf();
+ break;
+ case 'D':
+ // CUB - Cursor backward
+ cub();
+ break;
+ case 'E':
+ // CNL - Cursor down and to column 1
+ if (type == DeviceType.XTERM) {
+ cnl();
+ }
+ break;
+ case 'F':
+ // CPL - Cursor up and to column 1
+ if (type == DeviceType.XTERM) {
+ cpl();
+ }
+ break;
+ case 'G':
+ // CHA - Cursor to column # in current row
+ if (type == DeviceType.XTERM) {
+ cha();
+ }
+ break;
+ case 'H':
+ // CUP - Cursor position
+ cup();
+ break;
+ case 'I':
+ // CHT - Cursor forward X tab stops (default 1)
+ if (type == DeviceType.XTERM) {
+ cht();
+ }
+ break;
+ case 'J':
+ // ED - Erase in display
+ ed();
+ break;
+ case 'K':
+ // EL - Erase in line
+ el();
+ break;
+ case 'L':
+ // IL - Insert line
+ il();
+ break;
+ case 'M':
+ // DL - Delete line
+ dl();
+ break;
+ case 'N':
+ case 'O':
+ break;
+ case 'P':
+ // DCH - Delete character
+ dch();
+ break;
+ case 'Q':
+ case 'R':
+ break;
+ case 'S':
+ // Scroll up X lines (default 1)
+ if (type == DeviceType.XTERM) {
+ boolean xtermPrivateModeFlag = false;
+ for (int i = 0; i < collectBuffer.length(); i++) {
+ if (collectBuffer.charAt(i) == '?') {
+ xtermPrivateModeFlag = true;
+ break;
+ }
+ }
+ if (xtermPrivateModeFlag) {
+ xtermSixelQuery();
+ } else {
+ su();
+ }
+ }
+ break;
+ case 'T':
+ // Scroll down X lines (default 1)
+ if (type == DeviceType.XTERM) {
+ sd();
+ }
+ break;
+ case 'U':
+ case 'V':
+ case 'W':
+ break;
+ case 'X':
+ if ((type == DeviceType.VT220)
+ || (type == DeviceType.XTERM)) {
+
+ // ECH - Erase character
+ ech();
+ }
+ break;
+ case 'Y':
+ break;
+ case 'Z':
+ // CBT - Cursor backward X tab stops (default 1)
+ if (type == DeviceType.XTERM) {
+ cbt();
+ }
+ break;
+ case '[':
+ case '\\':
+ case ']':
+ case '^':
+ case '_':
+ break;
+ case '`':
+ // HPA - Cursor to column # in current row. Same as CHA
+ if (type == DeviceType.XTERM) {
+ cha();
+ }
+ break;
+ case 'a':
+ // HPR - Cursor right. Same as CUF
+ if (type == DeviceType.XTERM) {
+ cuf();
+ }
+ break;
+ case 'b':
+ // REP - Repeat last char X times
+ if (type == DeviceType.XTERM) {
+ rep();
+ }
+ break;
+ case 'c':
+ // DA - Device attributes
+ da();
+ break;
+ case 'd':
+ // VPA - Cursor to row, current column.
+ if (type == DeviceType.XTERM) {
+ vpa();
+ }
+ break;
+ case 'e':
+ // VPR - Cursor down. Same as CUD
+ if (type == DeviceType.XTERM) {
+ cud();
+ }
+ break;
+ case 'f':
+ // HVP - Horizontal and vertical position
+ hvp();
+ break;
+ case 'g':
+ // TBC - Tabulation clear
+ tbc();
+ break;
+ case 'h':
+ // Sets an ANSI or DEC private toggle
+ setToggle(true);
+ break;
+ case 'i':
+ if ((type == DeviceType.VT220)
+ || (type == DeviceType.XTERM)) {
+
+ // Printer functions
+ printerFunctions();
+ }
+ break;
+ case 'j':
+ case 'k':
+ break;
+ case 'l':
+ // Sets an ANSI or DEC private toggle
+ setToggle(false);
+ break;
+ case 'm':
+ // SGR - Select graphics rendition
+ sgr();
+ break;
+ case 'n':
+ // DSR - Device status report
+ dsr();
+ break;
+ case 'o':
+ case 'p':
+ break;
+ case 'q':
+ // DECLL - Load leds
+ // Not supported
+ break;
+ case 'r':
+ // DECSTBM - Set top and bottom margins
+ decstbm();
+ break;
+ case 's':
+ break;
+ case 't':
+ if (type == DeviceType.XTERM) {
+ // Window operations
+ xtermWindowOps();
+ }
+ break;
+ case 'u':
+ case 'v':
+ case 'w':
+ break;
+ case 'x':
+ // DECREQTPARM - Request terminal parameters
+ decreqtparm();
+ break;
+ case 'y':
+ case 'z':
+ case '{':
+ case '|':
+ case '}':
+ case '~':
+ break;
+ }
+ toGround();
+ }
+
+ // 7F --> ignore
+ return;
+
+ case CSI_INTERMEDIATE:
+ // 00-17, 19, 1C-1F --> execute
+ if (ch <= 0x1F) {
+ handleControlChar((char) ch);
+ }
+
+ // 20-2F --> collect
+ if ((ch >= 0x20) && (ch <= 0x2F)) {
+ collect((char) ch);
+ }
+
+ // 0x30-3F goes to CSI_IGNORE
+ if ((ch >= 0x30) && (ch <= 0x3F)) {
+ scanState = ScanState.CSI_IGNORE;
+ }
+
+ // 40-7E --> dispatch, then switch to GROUND
+ if ((ch >= 0x40) && (ch <= 0x7E)) {
+ switch (ch) {
+ case '@':
+ case 'A':
+ case 'B':
+ case 'C':
+ case 'D':
+ case 'E':
+ case 'F':
+ case 'G':
+ case 'H':
+ case 'I':
+ case 'J':
+ case 'K':
+ case 'L':
+ case 'M':
+ case 'N':
+ case 'O':
+ case 'P':
+ case 'Q':
+ case 'R':
+ case 'S':
+ case 'T':
+ case 'U':
+ case 'V':
+ case 'W':
+ case 'X':
+ case 'Y':
+ case 'Z':
+ case '[':
+ case '\\':
+ case ']':
+ case '^':
+ case '_':
+ case '`':
+ case 'a':
+ case 'b':
+ case 'c':
+ case 'd':
+ case 'e':
+ case 'f':
+ case 'g':
+ case 'h':
+ case 'i':
+ case 'j':
+ case 'k':
+ case 'l':
+ case 'm':
+ case 'n':
+ case 'o':
+ break;
+ case 'p':
+ if (((type == DeviceType.VT220)
+ || (type == DeviceType.XTERM))
+ && (collectBuffer.charAt(collectBuffer.length() - 1) == '\"')
+ ) {
+ // DECSCL - compatibility level
+ decscl();
+ }
+ if ((type == DeviceType.XTERM)
+ && (collectBuffer.charAt(collectBuffer.length() - 1) == '!')
+ ) {
+ // DECSTR - Soft terminal reset
+ decstr();
+ }
+ break;
+ case 'q':
+ if (((type == DeviceType.VT220)
+ || (type == DeviceType.XTERM))
+ && (collectBuffer.charAt(collectBuffer.length() - 1) == '\"')
+ ) {
+ // DECSCA
+ decsca();
+ }
+ break;
+ case 'r':
+ case 's':
+ case 't':
+ case 'u':
+ case 'v':
+ case 'w':
+ case 'x':
+ case 'y':
+ case 'z':
+ case '{':
+ case '|':
+ case '}':
+ case '~':
+ break;
+ }
+ toGround();
+ }
+
+ // 7F --> ignore
+ return;
+
+ case CSI_IGNORE:
+ // 00-17, 19, 1C-1F --> execute
+ if (ch <= 0x1F) {
+ handleControlChar((char) ch);
+ }
+
+ // 20-2F --> collect
+ if ((ch >= 0x20) && (ch <= 0x2F)) {
+ collect((char) ch);
+ }
+
+ // 40-7E --> ignore, then switch to GROUND
+ if ((ch >= 0x40) && (ch <= 0x7E)) {
+ toGround();
+ }
+
+ // 20-3F, 7F --> ignore
+
+ return;
+
+ case DCS_ENTRY:
+
+ // 0x9C goes to GROUND
+ if (ch == 0x9C) {
+ toGround();
+ }
+
+ // 0x1B 0x5C goes to GROUND
+ if (ch == 0x1B) {
+ collect((char) ch);
+ }
+ if (ch == 0x5C) {
+ if ((collectBuffer.length() > 0)
+ && (collectBuffer.charAt(collectBuffer.length() - 1) == 0x1B)
+ ) {
+ toGround();
+ }
+ }
+
+ // 20-2F --> collect, then switch to DCS_INTERMEDIATE
+ if ((ch >= 0x20) && (ch <= 0x2F)) {
+ collect((char) ch);
+ scanState = ScanState.DCS_INTERMEDIATE;
+ }
+
+ // 30-39, 3B --> param, then switch to DCS_PARAM
+ if ((ch >= '0') && (ch <= '9')) {
+ param((byte) ch);
+ scanState = ScanState.DCS_PARAM;
+ }
+ if (ch == ';') {
+ param((byte) ch);
+ scanState = ScanState.DCS_PARAM;
+ }
+
+ // 3C-3F --> collect, then switch to DCS_PARAM
+ if ((ch >= 0x3C) && (ch <= 0x3F)) {
+ collect((char) ch);
+ scanState = ScanState.DCS_PARAM;
+ }
+
+ // 00-17, 19, 1C-1F, 7F --> ignore
+
+ // 0x3A goes to DCS_IGNORE
+ if (ch == 0x3F) {
+ scanState = ScanState.DCS_IGNORE;
+ }
+
+ // 0x71 goes to DCS_SIXEL
+ if (ch == 0x71) {
+ sixelParseBuffer = new StringBuilder();
+ scanState = ScanState.DCS_SIXEL;
+ } else if ((ch >= 0x40) && (ch <= 0x7E)) {
+ // 0x40-7E goes to DCS_PASSTHROUGH
+ scanState = ScanState.DCS_PASSTHROUGH;
+ }
+ return;
+
+ case DCS_INTERMEDIATE:
+
+ // 0x9C goes to GROUND
+ if (ch == 0x9C) {
+ toGround();
+ }
+
+ // 0x1B 0x5C goes to GROUND
+ if (ch == 0x1B) {
+ collect((char) ch);
+ }
+ if (ch == 0x5C) {
+ if ((collectBuffer.length() > 0)
+ && (collectBuffer.charAt(collectBuffer.length() - 1) == 0x1B)
+ ) {
+ toGround();
+ }
+ }
+
+ // 0x30-3F goes to DCS_IGNORE
+ if ((ch >= 0x30) && (ch <= 0x3F)) {
+ scanState = ScanState.DCS_IGNORE;
+ }
+
+ // 0x40-7E goes to DCS_PASSTHROUGH
+ if ((ch >= 0x40) && (ch <= 0x7E)) {
+ scanState = ScanState.DCS_PASSTHROUGH;
+ }
+
+ // 00-17, 19, 1C-1F, 7F --> ignore
+ return;
+
+ case DCS_PARAM:
+
+ // 0x9C goes to GROUND
+ if (ch == 0x9C) {
+ toGround();
+ }
+
+ // 0x1B 0x5C goes to GROUND
+ if (ch == 0x1B) {
+ collect((char) ch);
+ }
+ if (ch == 0x5C) {
+ if ((collectBuffer.length() > 0)
+ && (collectBuffer.charAt(collectBuffer.length() - 1) == 0x1B)
+ ) {
+ toGround();
+ }
+ }
+
+ // 20-2F --> collect, then switch to DCS_INTERMEDIATE
+ if ((ch >= 0x20) && (ch <= 0x2F)) {
+ collect((char) ch);
+ scanState = ScanState.DCS_INTERMEDIATE;
+ }
+
+ // 30-39, 3B --> param
+ if ((ch >= '0') && (ch <= '9')) {
+ param((byte) ch);
+ }
+ if (ch == ';') {
+ param((byte) ch);
+ }
+
+ // 00-17, 19, 1C-1F, 7F --> ignore
+
+ // 0x3A, 3C-3F goes to DCS_IGNORE
+ if (ch == 0x3F) {
+ scanState = ScanState.DCS_IGNORE;
+ }
+ if ((ch >= 0x3C) && (ch <= 0x3F)) {
+ scanState = ScanState.DCS_IGNORE;
+ }
+
+ // 0x71 goes to DCS_SIXEL
+ if (ch == 0x71) {
+ sixelParseBuffer = new StringBuilder();
+ scanState = ScanState.DCS_SIXEL;
+ } else if ((ch >= 0x40) && (ch <= 0x7E)) {
+ // 0x40-7E goes to DCS_PASSTHROUGH
+ scanState = ScanState.DCS_PASSTHROUGH;
+ }
+ return;
+
+ case DCS_PASSTHROUGH:
+ // 0x9C goes to GROUND
+ if (ch == 0x9C) {
+ toGround();
+ }
+
+ // 0x1B 0x5C goes to GROUND
+ if (ch == 0x1B) {
+ collect((char) ch);
+ }
+ if (ch == 0x5C) {
+ if ((collectBuffer.length() > 0)
+ && (collectBuffer.charAt(collectBuffer.length() - 1) == 0x1B)
+ ) {
+ toGround();
+ }
+ }
+
+ // 00-17, 19, 1C-1F, 20-7E --> put
+ if (ch <= 0x17) {
+ // We ignore all DCS except sixel.
+ return;
+ }
+ if (ch == 0x19) {
+ // We ignore all DCS except sixel.
+ return;
+ }
+ if ((ch >= 0x1C) && (ch <= 0x1F)) {
+ // We ignore all DCS except sixel.
+ return;
+ }
+ if ((ch >= 0x20) && (ch <= 0x7E)) {
+ // We ignore all DCS except sixel.
+ return;
+ }
+
+ // 7F --> ignore
+
+ return;
+
+ case DCS_IGNORE:
+ // 00-17, 19, 1C-1F, 20-7F --> ignore
+
+ // 0x9C goes to GROUND
+ if (ch == 0x9C) {
+ toGround();
+ }
+
+ return;
+
+ case DCS_SIXEL:
+ // 0x9C goes to GROUND
+ if (ch == 0x9C) {
+ parseSixel();
+ toGround();
+ return;
+ }
+
+ // 0x1B 0x5C goes to GROUND
+ if (ch == 0x1B) {
+ collect((char) ch);
+ return;
+ }
+ if (ch == 0x5C) {
+ if ((collectBuffer.length() > 0)
+ && (collectBuffer.charAt(collectBuffer.length() - 1) == 0x1B)
+ ) {
+ parseSixel();
+ toGround();
+ return;
+ }
+ }
+
+ // 00-17, 19, 1C-1F, 20-7E --> put
+ if ((ch <= 0x17)
+ || (ch == 0x19)
+ || ((ch >= 0x1C) && (ch <= 0x1F))
+ || ((ch >= 0x20) && (ch <= 0x7E))
+ ) {
+ sixelParseBuffer.append((char) ch);
+ }
+
+ // 7F --> ignore
+ return;
+
+ case SOSPMAPC_STRING:
+ // 00-17, 19, 1C-1F, 20-7F --> ignore
+
+ // Special case for Jexer: PM can pass one control character
+ if (ch == 0x1B) {
+ pmPut((char) ch);
+ }
+
+ if ((ch >= 0x20) && (ch <= 0x7F)) {
+ pmPut((char) ch);
+ }
+
+ // 0x9C goes to GROUND
+ if (ch == 0x9C) {
+ toGround();
+ }
+
+ return;
+
+ case OSC_STRING:
+ // Special case for Xterm: OSC can pass control characters
+ if ((ch == 0x9C) || (ch == 0x07) || (ch == 0x1B)) {
+ oscPut((char) ch);
+ }
+
+ // 00-17, 19, 1C-1F --> ignore
+
+ // 20-7F --> osc_put
+ if ((ch >= 0x20) && (ch <= 0x7F)) {
+ oscPut((char) ch);
+ }
+
+ // 0x9C goes to GROUND
+ if (ch == 0x9C) {
+ toGround();
+ }
+
+ return;
+
+ case VT52_DIRECT_CURSOR_ADDRESS:
+ // This is a special case for the VT52 sequence "ESC Y l c"
+ if (collectBuffer.length() == 0) {
+ collect((char) ch);
+ } else if (collectBuffer.length() == 1) {
+ // We've got the two characters, one in the buffer and the
+ // other in ch.
+ cursorPosition(collectBuffer.charAt(0) - '\040', ch - '\040');
+ toGround();
+ }
+ return;
+ }
+
+ }
+
+ /**
+ * Expose current cursor X to outside world.
+ *
+ * @return current cursor X
+ */
+ public final int getCursorX() {
+ if (display.get(currentState.cursorY).isDoubleWidth()) {
+ return currentState.cursorX * 2;
+ }
+ return currentState.cursorX;
+ }
+
+ /**
+ * Expose current cursor Y to outside world.
+ *
+ * @return current cursor Y
+ */
+ public final int getCursorY() {
+ return currentState.cursorY;
+ }
+
+ /**
+ * Returns true if this terminal has requested the mouse pointer be
+ * hidden.
+ *
+ * @return true if this terminal has requested the mouse pointer be
+ * hidden
+ */
+ public final boolean hasHiddenMousePointer() {
+ return hideMousePointer;
+ }
+
+ /**
+ * Get the mouse protocol.
+ *
+ * @return MouseProtocol.OFF, MouseProtocol.X10, etc.
+ */
+ public MouseProtocol getMouseProtocol() {
+ return mouseProtocol;
+ }
+
+ /**
+ * Draw the left and right cells of a two-cell-wide (full-width) glyph.
+ *
+ * @param leftX the x position to draw the left half to
+ * @param leftY the y position to draw the left half to
+ * @param rightX the x position to draw the right half to
+ * @param rightY the y position to draw the right half to
+ * @param ch the character to draw
+ */
+ private void drawHalves(final int leftX, final int leftY,
+ final int rightX, final int rightY, final int ch) {
+
+ // System.err.println("drawHalves(): " + Integer.toHexString(ch));
+
+ if (lastTextHeight != textHeight) {
+ glyphMaker = GlyphMaker.getInstance(textHeight);
+ lastTextHeight = textHeight;
+ }
+
+ Cell cell = new Cell(ch, currentState.attr);
+ BufferedImage image = glyphMaker.getImage(cell, textWidth * 2,
+ textHeight);
+ BufferedImage leftImage = image.getSubimage(0, 0, textWidth,
+ textHeight);
+ BufferedImage rightImage = image.getSubimage(textWidth, 0, textWidth,
+ textHeight);
+
+ Cell left = new Cell(cell);
+ left.setImage(leftImage);
+ left.setWidth(Cell.Width.LEFT);
+ display.get(leftY).replace(leftX, left);
+
+ Cell right = new Cell(cell);
+ right.setImage(rightImage);
+ right.setWidth(Cell.Width.RIGHT);
+ display.get(rightY).replace(rightX, right);
+ }
+
+ /**
+ * Set the width of a character cell in pixels.
+ *
+ * @param textWidth the width in pixels of a character cell
+ */
+ public void setTextWidth(final int textWidth) {
+ this.textWidth = textWidth;
+ }
+
+ /**
+ * Set the height of a character cell in pixels.
+ *
+ * @param textHeight the height in pixels of a character cell
+ */
+ public void setTextHeight(final int textHeight) {
+ this.textHeight = textHeight;
+ }
+
+ /**
+ * Parse a sixel string into a bitmap image, and overlay that image onto
+ * the text cells.
+ */
+ private void parseSixel() {
+
+ /*
+ System.err.println("parseSixel(): '" + sixelParseBuffer.toString()
+ + "'");
+ */
+
+ Sixel sixel = new Sixel(sixelParseBuffer.toString(), sixelPalette);
+ BufferedImage image = sixel.getImage();
+
+ // System.err.println("parseSixel(): image " + image);
+
+ if (image == null) {
+ // 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);
+ }
+
+ }
+
+ /**
+ * Parse a "Jexer" image string into a bitmap image, and overlay that
+ * image onto the text cells.
+ *
+ * @param pw width token
+ * @param ph height token
+ * @param ps scroll token
+ * @param data pixel data
+ */
+ private void parseJexerImage(final String pw, final String ph,
+ final String ps, final String data) {
+
+ int imageWidth = 0;
+ int imageHeight = 0;
+ boolean scroll = false;
+ try {
+ imageWidth = Integer.parseInt(pw);
+ imageHeight = Integer.parseInt(ph);
+ } catch (NumberFormatException e) {
+ // SQUASH
+ return;
+ }
+ 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;
+ }
+
+ java.util.Base64.Decoder base64 = java.util.Base64.getDecoder();
+ byte [] bytes = base64.decode(data);
+ if (bytes.length != (imageWidth * imageHeight * 3)) {
+ return;
+ }
+
+ BufferedImage image = new BufferedImage(imageWidth, imageHeight,
+ BufferedImage.TYPE_INT_ARGB);
+
+ for (int x = 0; x < imageWidth; x++) {
+ for (int y = 0; y < imageHeight; y++) {
+ int red = bytes[(y * imageWidth * 3) + (x * 3) ];
+ if (red < 0) {
+ red += 256;
+ }
+ int green = bytes[(y * imageWidth * 3) + (x * 3) + 1];
+ if (green < 0) {
+ green += 256;
+ }
+ int blue = bytes[(y * imageWidth * 3) + (x * 3) + 2];
+ if (blue < 0) {
+ blue += 256;
+ }
+ int rgb = 0xFF000000 | (red << 16) | (green << 8) | blue;
+ image.setRGB(x, y, rgb);
+ }
+ }
+
+ /*
+ * 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);
+ 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))
+ ) {
+ linefeed();
+ }
+ cursorPosition(currentState.cursorY, x0);
+ }
+
+ }
+
+ }
--- /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.tterminal;
+
+ import java.awt.Color;
+ import java.awt.Graphics2D;
+ import java.awt.image.BufferedImage;
+ import java.util.ArrayList;
+ import java.util.HashMap;
+
+ /**
+ * Sixel parses a buffer of sixel image data into a BufferedImage.
+ */
+ public class Sixel {
+
+ // ------------------------------------------------------------------------
+ // Constants --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Parser character scan states.
+ */
+ private enum ScanState {
+ GROUND,
+ RASTER,
+ COLOR,
+ REPEAT,
+ }
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * If true, enable debug messages.
+ */
+ private static boolean DEBUG = false;
+
+ /**
+ * Number of pixels to increment when we need more horizontal room.
+ */
+ private static int WIDTH_INCREASE = 400;
+
+ /**
+ * Number of pixels to increment when we need more vertical room.
+ */
+ private static int HEIGHT_INCREASE = 400;
+
+ /**
+ * Maximum width in pixels.
+ */
+ private static int MAX_WIDTH = 1000;
+
+ /**
+ * Maximum height in pixels.
+ */
+ private static int MAX_HEIGHT = 1000;
+
+ /**
+ * Current scanning state.
+ */
+ private ScanState scanState = ScanState.GROUND;
+
+ /**
+ * Parameters being collected.
+ */
+ private int [] params = new int[5];
+
+ /**
+ * Current parameter being collected.
+ */
+ private int paramsI = 0;
+
+ /**
+ * The sixel palette colors specified.
+ */
+ private HashMap<Integer, Color> palette;
+
+ /**
+ * The buffer to parse.
+ */
+ private String buffer;
+
+ /**
+ * The image being drawn to.
+ */
+ private BufferedImage image;
+
+ /**
+ * The real width of image.
+ */
+ private int width = 0;
+
+ /**
+ * The real height of image.
+ */
+ private int height = 0;
+
+ /**
+ * The width of image provided in the raster attribute.
+ */
+ private int rasterWidth = 0;
+
+ /**
+ * The height of image provided in the raster attribute.
+ */
+ private int rasterHeight = 0;
+
+ /**
+ * The repeat count.
+ */
+ private int repeatCount = -1;
+
+ /**
+ * The current drawing x position.
+ */
+ private int x = 0;
+
+ /**
+ * The maximum y drawn to. This will set the final image height.
+ */
+ private int y = 0;
+
+ /**
+ * The current drawing color.
+ */
+ private Color color = Color.BLACK;
+
+ /**
+ * If set, abort processing this image.
+ */
+ private boolean abort = false;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param buffer the sixel data to parse
+ * @param palette palette to use, or null for a private palette
+ */
+ public Sixel(final String buffer, final HashMap<Integer, Color> palette) {
+ this.buffer = buffer;
+ if (palette == null) {
+ this.palette = new HashMap<Integer, Color>();
+ } else {
+ this.palette = palette;
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // Sixel ------------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get the image.
+ *
+ * @return the sixel data as an image.
+ */
+ public BufferedImage getImage() {
+ if (buffer != null) {
+ for (int i = 0; (i < buffer.length()) && (abort == false); i++) {
+ consume(buffer.charAt(i));
+ }
+ buffer = null;
+ }
+ if (abort == true) {
+ return null;
+ }
+
+ if ((width > 0) && (height > 0) && (image != null)) {
+ /*
+ System.err.println(String.format("%d %d %d %d", width, y + 1,
+ rasterWidth, rasterHeight));
+ */
+
+ if ((rasterWidth > width) || (rasterHeight > y + 1)) {
+ resizeImage(Math.max(width, rasterWidth),
+ Math.max(y + 1, rasterHeight));
+ }
+ return image.getSubimage(0, 0, width, y + 1);
+ }
+ return null;
+ }
+
+ /**
+ * Resize image to a new size.
+ *
+ * @param newWidth new width of image
+ * @param newHeight new height of image
+ */
+ private void resizeImage(final int newWidth, final int newHeight) {
+ BufferedImage newImage = new BufferedImage(newWidth, newHeight,
+ BufferedImage.TYPE_INT_ARGB);
+
+ if (image == null) {
+ image = newImage;
+ return;
+ }
+
+ if (DEBUG) {
+ System.err.println("resizeImage(); old " + image.getWidth() + "x" +
+ image.getHeight() + " new " + newWidth + "x" + newHeight);
+ }
+
+ Graphics2D gr = newImage.createGraphics();
+ gr.drawImage(image, 0, 0, image.getWidth(), image.getHeight(), null);
+ gr.dispose();
+ image = newImage;
+ }
+
+ /**
+ * Clear the parameters and flags.
+ */
+ private void toGround() {
+ paramsI = 0;
+ for (int i = 0; i < params.length; i++) {
+ params[i] = 0;
+ }
+ scanState = ScanState.GROUND;
+ repeatCount = -1;
+ }
+
+ /**
+ * Get a color parameter value, with a default.
+ *
+ * @param position parameter index. 0 is the first parameter.
+ * @param defaultValue value to use if colorParams[position] doesn't exist
+ * @return parameter value
+ */
+ private int getParam(final int position, final int defaultValue) {
+ if (position > paramsI) {
+ return defaultValue;
+ }
+ return params[position];
+ }
+
+ /**
+ * Get a color parameter value, clamped to within min/max.
+ *
+ * @param position parameter index. 0 is the first parameter.
+ * @param defaultValue value to use if colorParams[position] doesn't exist
+ * @param minValue minimum value inclusive
+ * @param maxValue maximum value inclusive
+ * @return parameter value
+ */
+ private int getParam(final int position, final int defaultValue,
+ final int minValue, final int maxValue) {
+
+ assert (minValue <= maxValue);
+ int value = getParam(position, defaultValue);
+ if (value < minValue) {
+ value = minValue;
+ }
+ if (value > maxValue) {
+ value = maxValue;
+ }
+ return value;
+ }
+
+ /**
+ * Add sixel data to the image.
+ *
+ * @param ch the character of sixel data
+ */
+ private void addSixel(final char ch) {
+ int n = ((int) ch - 63);
+
+ if (DEBUG && (color == null)) {
+ System.err.println("color is null?!");
+ System.err.println(buffer);
+ }
+
+ int rgb = color.getRGB();
+ int rep = (repeatCount == -1 ? 1 : repeatCount);
+
+ if (DEBUG) {
+ System.err.println("addSixel() rep " + rep + " char " +
+ Integer.toHexString(n) + " color " + color);
+ }
+
+ assert (n >= 0);
+
+ if (image == null) {
+ // The raster attributes was not provided.
+ resizeImage(WIDTH_INCREASE, HEIGHT_INCREASE);
+ }
+
+ if (x + rep > image.getWidth()) {
+ // Resize the image, give us another max(rep, WIDTH_INCREASE)
+ // pixels of horizontal length.
+ resizeImage(image.getWidth() + Math.max(rep, WIDTH_INCREASE),
+ image.getHeight());
+ }
+
+ // If nothing will be drawn, just advance x.
+ if (n == 0) {
+ x += rep;
+ if (x > width) {
+ width = x;
+ }
+ if (width > MAX_WIDTH) {
+ abort = true;
+ }
+ return;
+ }
+
+ int dy = 0;
+ for (int i = 0; i < rep; i++) {
+ if ((n & 0x01) != 0) {
+ dy = 0;
+ image.setRGB(x, height + dy, rgb);
+ }
+ if ((n & 0x02) != 0) {
+ dy = 1;
+ image.setRGB(x, height + dy, rgb);
+ }
+ if ((n & 0x04) != 0) {
+ dy = 2;
+ image.setRGB(x, height + dy, rgb);
+ }
+ if ((n & 0x08) != 0) {
+ dy = 3;
+ image.setRGB(x, height + dy, rgb);
+ }
+ if ((n & 0x10) != 0) {
+ dy = 4;
+ image.setRGB(x, height + dy, rgb);
+ }
+ if ((n & 0x20) != 0) {
+ dy = 5;
+ image.setRGB(x, height + dy, rgb);
+ }
+ if (height + dy > y) {
+ y = height + dy;
+ }
+ x++;
+ }
+ if (x > width) {
+ width = x;
+ }
+ if (width > MAX_WIDTH) {
+ abort = true;
+ }
+ if (y + 1 > MAX_HEIGHT) {
+ abort = true;
+ }
+ }
+
+ /**
+ * Process a color palette change.
+ */
+ private void setPalette() {
+ int idx = getParam(0, 0);
+
+ if (paramsI == 0) {
+ Color newColor = palette.get(idx);
+ if (newColor != null) {
+ color = newColor;
+ } else {
+ if (DEBUG) {
+ System.err.println("COLOR " + idx + " NOT FOUND");
+ }
+ color = Color.BLACK;
+ }
+
+ if (DEBUG) {
+ System.err.println("set color " + idx + " " + color);
+ }
+ return;
+ }
+
+ int type = getParam(1, 0);
+ float red = (float) (getParam(2, 0, 0, 100) / 100.0);
+ float green = (float) (getParam(3, 0, 0, 100) / 100.0);
+ float blue = (float) (getParam(4, 0, 0, 100) / 100.0);
+
+ if (type == 2) {
+ Color newColor = new Color(red, green, blue);
+ palette.put(idx, newColor);
+ if (DEBUG) {
+ System.err.println("Palette color " + idx + " --> " + newColor);
+ }
+ } else {
+ if (DEBUG) {
+ System.err.println("UNKNOWN COLOR TYPE " + type + ": " + type +
+ " " + idx + " R " + red + " G " + green + " B " + blue);
+ }
+ }
+ }
+
+ /**
+ * Parse the raster attributes.
+ */
+ private void parseRaster() {
+ int pan = getParam(0, 0); // Aspect ratio numerator
+ int pad = getParam(1, 0); // Aspect ratio denominator
+ int pah = getParam(2, 0); // Horizontal width
+ int pav = getParam(3, 0); // Vertical height
+
+ if ((pan == pad) && (pah > 0) && (pav > 0)) {
+ rasterWidth = pah;
+ rasterHeight = pav;
+ if ((rasterWidth <= MAX_WIDTH) && (rasterHeight <= MAX_HEIGHT)) {
+ resizeImage(rasterWidth, rasterHeight);
+ } else {
+ abort = true;
+ }
+ } else {
+ abort = true;
+ }
+ }
+
+ /**
+ * Run this input character through the sixel state machine.
+ *
+ * @param ch character from the remote side
+ */
+ private void consume(char ch) {
+
+ // DEBUG
+ // System.err.printf("Sixel.consume() %c STATE = %s\n", ch, scanState);
+
+ // Between decimal 63 (inclusive) and 127 (exclusive) --> pixels
+ if ((ch >= 63) && (ch < 127)) {
+ if (scanState == ScanState.COLOR) {
+ setPalette();
+ }
+ if (scanState == ScanState.RASTER) {
+ parseRaster();
+ toGround();
+ }
+ addSixel(ch);
+ toGround();
+ return;
+ }
+
+ if (ch == '#') {
+ // Next color is here, parse what we had before.
+ if (scanState == ScanState.COLOR) {
+ setPalette();
+ toGround();
+ }
+ if (scanState == ScanState.RASTER) {
+ parseRaster();
+ toGround();
+ }
+ scanState = ScanState.COLOR;
+ return;
+ }
+
+ if (ch == '!') {
+ // Repeat count
+ if (scanState == ScanState.COLOR) {
+ setPalette();
+ toGround();
+ }
+ if (scanState == ScanState.RASTER) {
+ parseRaster();
+ toGround();
+ }
+ scanState = ScanState.REPEAT;
+ repeatCount = 0;
+ return;
+ }
+
+ if (ch == '-') {
+ if (scanState == ScanState.COLOR) {
+ setPalette();
+ toGround();
+ }
+ if (scanState == ScanState.RASTER) {
+ parseRaster();
+ toGround();
+ }
+
+ height += 6;
+ x = 0;
+
+ if (height + 6 > image.getHeight()) {
+ // Resize the image, give us another HEIGHT_INCREASE
+ // pixels of vertical length.
+ resizeImage(image.getWidth(),
+ image.getHeight() + HEIGHT_INCREASE);
+ }
+ return;
+ }
+
+ if (ch == '$') {
+ if (scanState == ScanState.COLOR) {
+ setPalette();
+ toGround();
+ }
+ if (scanState == ScanState.RASTER) {
+ parseRaster();
+ toGround();
+ }
+ x = 0;
+ return;
+ }
+
+ if (ch == '"') {
+ if (scanState == ScanState.COLOR) {
+ setPalette();
+ toGround();
+ }
+ scanState = ScanState.RASTER;
+ return;
+ }
+
+ switch (scanState) {
+
+ case GROUND:
+ // Unknown character.
+ if (DEBUG) {
+ System.err.println("UNKNOWN CHAR: " + ch);
+ }
+ return;
+
+ case RASTER:
+ // 30-39, 3B --> param
+ if ((ch >= '0') && (ch <= '9')) {
+ params[paramsI] *= 10;
+ params[paramsI] += (ch - '0');
+ }
+ if (ch == ';') {
+ if (paramsI < params.length - 1) {
+ paramsI++;
+ }
+ }
+ return;
+
+ case COLOR:
+ // 30-39, 3B --> param
+ if ((ch >= '0') && (ch <= '9')) {
+ params[paramsI] *= 10;
+ params[paramsI] += (ch - '0');
+ }
+ if (ch == ';') {
+ if (paramsI < params.length - 1) {
+ paramsI++;
+ }
+ }
+ return;
+
+ case REPEAT:
+ if ((ch >= '0') && (ch <= '9')) {
+ if (repeatCount == -1) {
+ repeatCount = (int) (ch - '0');
+ } else {
+ repeatCount *= 10;
+ repeatCount += (int) (ch - '0');
+ }
+ }
+ return;
+
+ }
+
+ }
+
+ }
--- /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
+ */
+
+ /**
+ * An ECMA-48 / ANSI X3.64 style terminal emulator.
+ */
+ package jexer.tterminal;
--- /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.ttree;
+
+ import java.io.File;
+ import java.io.IOException;
+ import java.util.Collections;
+ import java.util.List;
+ import java.util.LinkedList;
+
+ import jexer.TWidget;
+
+ /**
+ * TDirectoryTreeItem is a single item in a disk directory tree view.
+ */
+ public class TDirectoryTreeItem extends TTreeItem {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * File corresponding to this list item.
+ */
+ private File file;
+
+ /**
+ * The TTreeViewWidget containing this directory tree.
+ */
+ private TTreeViewWidget treeViewWidget;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param view root TTreeViewWidget
+ * @param text text for this item
+ * @param expanded if true, have it expanded immediately
+ * @throws IOException if a java.io operation throws
+ */
+ public TDirectoryTreeItem(final TTreeViewWidget view, final String text,
+ final boolean expanded) throws IOException {
+
+ this(view, text, expanded, true);
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param view root TTreeViewWidget
+ * @param text text for this item
+ * @param expanded if true, have it expanded immediately
+ * @param openParents if true, expand all paths up the root path and
+ * return the root path entry
+ * @throws IOException if a java.io operation throws
+ */
+ public TDirectoryTreeItem(final TTreeViewWidget view, final String text,
+ final boolean expanded, final boolean openParents) throws IOException {
+
+ super(view.getTreeView(), text, false);
+
+ this.treeViewWidget = view;
+
+ List<String> parentFiles = new LinkedList<String>();
+ boolean oldExpanded = expanded;
+
+ // Convert to canonical path
+ File rootFile = new File(text);
+ rootFile = rootFile.getCanonicalFile();
+
+ if (openParents) {
+ setExpanded(true);
+
+ // Go up the directory tree
+ File parent = rootFile.getParentFile();
+ while (parent != null) {
+ parentFiles.add(rootFile.getName());
+ rootFile = rootFile.getParentFile();
+ parent = rootFile.getParentFile();
+ }
+ }
+ file = rootFile;
+ if (rootFile.getParentFile() == null) {
+ // This is a filesystem root, use its full name
+ setText(rootFile.getCanonicalPath());
+ } else {
+ // This is a relative path. We got here because openParents was
+ // false.
+ assert (!openParents);
+ setText(rootFile.getName());
+ }
+ onExpand();
+
+ if (openParents) {
+ TDirectoryTreeItem childFile = this;
+ Collections.reverse(parentFiles);
+ for (String p: parentFiles) {
+ for (TWidget widget: childFile.getChildren()) {
+ TDirectoryTreeItem child = (TDirectoryTreeItem) widget;
+ if (child.getText().equals(p)) {
+ childFile = child;
+ childFile.setExpanded(true);
+ childFile.onExpand();
+ break;
+ }
+ }
+ }
+ unselect();
+ getTreeView().setSelected(childFile, true);
+ setExpanded(oldExpanded);
+ }
+
+ view.reflowData();
+ }
+
+ // ------------------------------------------------------------------------
+ // TTreeItem --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get the File corresponding to this list item.
+ *
+ * @return the File
+ */
+ public final File getFile() {
+ return file;
+ }
+
+ /**
+ * Called when this item is expanded or collapsed. this.expanded will be
+ * true if this item was just expanded from a mouse click or keypress.
+ */
+ @Override
+ public final void onExpand() {
+ // System.err.printf("onExpand() %s\n", file);
+
+ if (file == null) {
+ return;
+ }
+ getChildren().clear();
+
+ // Make sure we can read it before trying to.
+ if (file.canRead()) {
+ setSelectable(true);
+ } else {
+ setSelectable(false);
+ }
+ assert (file.isDirectory());
+ setExpandable(true);
+
+ if (!isExpanded() || !isExpandable()) {
+ return;
+ }
+
+ File [] listFiles = file.listFiles();
+ if (listFiles != null) {
+ for (File f: listFiles) {
+ // System.err.printf(" -> file %s %s\n", file, file.getName());
+
+ if (f.getName().startsWith(".")) {
+ // Hide dot-files
+ continue;
+ }
+ if (!f.isDirectory()) {
+ continue;
+ }
+
+ try {
+ TDirectoryTreeItem item = new TDirectoryTreeItem(treeViewWidget,
+ f.getCanonicalPath(), false, false);
+
+ item.level = this.level + 1;
+ getChildren().add(item);
+ } catch (IOException e) {
+ continue;
+ }
+ }
+ }
+ Collections.sort(getChildren());
+ }
+
+ }
--- /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.ttree;
+
+ import java.util.ArrayList;
+ import java.util.List;
+
+ import jexer.TWidget;
+ import jexer.bits.CellAttributes;
+ import jexer.bits.GraphicsChars;
+ import jexer.bits.StringUtils;
+ import jexer.event.TKeypressEvent;
+ import jexer.event.TMouseEvent;
+ import static jexer.TKeypress.*;
+
+ /**
+ * TTreeItem is a single item in a tree view.
+ */
+ public class TTreeItem extends TWidget {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Hang onto reference to my parent TTreeView so I can call its reflow()
+ * when I add a child node.
+ */
+ private TTreeView view;
+
+ /**
+ * Displayable text for this item.
+ */
+ private String text;
+
+ /**
+ * If true, this item is expanded in the tree view.
+ */
+ private boolean expanded = true;
+
+ /**
+ * If true, this item can be expanded in the tree view.
+ */
+ private boolean expandable = false;
+
+ /**
+ * The vertical bars and such along the left side.
+ */
+ private String prefix = "";
+
+ /**
+ * Tree level.
+ */
+ protected int level = 0;
+
+ /**
+ * True means selected.
+ */
+ private boolean selected = false;
+
+ /**
+ * True means select-able.
+ */
+ private boolean selectable = true;
+
+ /**
+ * Whether or not this item is last in its parent's list of children.
+ */
+ private boolean last = false;
+
+ /**
+ * Pointer to the previous keyboard-navigable item (kbUp). Note package
+ * private access.
+ */
+ TTreeItem keyboardPrevious = null;
+
+ /**
+ * Pointer to the next keyboard-navigable item (kbDown). Note package
+ * private access.
+ */
+ TTreeItem keyboardNext = null;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param view root TTreeView
+ * @param text text for this item
+ * @param expanded if true, have it expanded immediately
+ */
+ public TTreeItem(final TTreeView view, final String text,
+ final boolean expanded) {
+
+ super(view, 0, 0, view.getWidth() - 3, 1);
+
+ this.text = text;
+ this.expanded = expanded;
+ this.view = view;
+
+ if (view.getTreeRoot() == null) {
+ view.setTreeRoot(this);
+ } else {
+ view.alignTree();
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // Event handlers ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Handle mouse release events.
+ *
+ * @param mouse mouse button release event
+ */
+ @Override
+ public void onMouseUp(final TMouseEvent mouse) {
+ if ((mouse.getX() == (getExpanderX() - view.getLeftColumn()))
+ && (mouse.getY() == 0)
+ ) {
+ if (level == 0) {
+ // Root node can't switch.
+ return;
+ }
+ if (selectable) {
+ // Flip expanded flag
+ expanded = !expanded;
+ if (expanded == false) {
+ // Unselect children that became invisible
+ unselect();
+ }
+ view.setSelected(this, false);
+ }
+ // Let subclasses do something with this
+ onExpand();
+
+ // Update the screen after any thing has expanded/contracted
+ view.alignTree();
+ } else if (mouse.getY() == 0) {
+ // Do the action associated with this item.
+ view.setSelected(this, false);
+ view.dispatch();
+ }
+ }
+
+ /**
+ * Called when this item is expanded or collapsed. this.expanded will be
+ * true if this item was just expanded from a mouse click or keypress.
+ */
+ public void onExpand() {
+ // Default: do nothing.
+ if (!expandable) {
+ return;
+ }
+ }
+
+ /**
+ * Handle keystrokes.
+ *
+ * @param keypress keystroke event
+ */
+ @Override
+ public void onKeypress(final TKeypressEvent keypress) {
+ if (keypress.equals(kbLeft)
+ || keypress.equals(kbRight)
+ || keypress.equals(kbSpace)
+ ) {
+ if (level == 0) {
+ // Root node can't switch.
+ return;
+ }
+ if (selectable) {
+ // Flip expanded flag
+ expanded = !expanded;
+ if (expanded == false) {
+ // Unselect children that became invisible
+ unselect();
+ }
+ view.setSelected(this, false);
+ }
+ // Let subclasses do something with this
+ onExpand();
+ } else if (keypress.equals(kbEnter)) {
+ // Do the action associated with this item.
+ view.dispatch();
+ } else {
+ // Pass other keys (tab etc.) on to TWidget's handler.
+ super.onKeypress(keypress);
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // TWidget ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Draw this item to a window.
+ */
+ @Override
+ public void draw() {
+ if ((getY() < 0) || (getY() > getParent().getHeight() - 1)) {
+ return;
+ }
+
+ int offset = -view.getLeftColumn();
+
+ CellAttributes color = getTheme().getColor("ttreeview");
+ CellAttributes textColor = getTheme().getColor("ttreeview");
+ CellAttributes expanderColor = getTheme().getColor("ttreeview.expandbutton");
+ CellAttributes selectedColor = getTheme().getColor("ttreeview.selected");
+
+ if (!getParent().isAbsoluteActive()) {
+ color = getTheme().getColor("ttreeview.inactive");
+ textColor = getTheme().getColor("ttreeview.inactive");
+ selectedColor = getTheme().getColor("ttreeview.selected.inactive");
+ }
+
+ if (!selectable) {
+ textColor = getTheme().getColor("ttreeview.unreadable");
+ }
+
+ // Blank out the background
+ hLineXY(0, 0, getWidth(), ' ', color);
+
+ String line = prefix;
+ if (level > 0) {
+ if (last) {
+ line += GraphicsChars.CP437[0xC0];
+ } else {
+ line += GraphicsChars.CP437[0xC3];
+ }
+ line += GraphicsChars.CP437[0xC4];
+ if (expandable) {
+ line += "[ ] ";
+ } else {
+ line += " ";
+ }
+ }
+ putStringXY(offset, 0, line, color);
+ if (selected) {
+ putStringXY(offset + StringUtils.width(line), 0, text, selectedColor);
+ } else {
+ putStringXY(offset + StringUtils.width(line), 0, text, textColor);
+ }
+ if ((level > 0) && (expandable)) {
+ if (expanded) {
+ putCharXY(offset + getExpanderX(), 0, '-', expanderColor);
+ } else {
+ putCharXY(offset + getExpanderX(), 0, '+', expanderColor);
+ }
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // TTreeItem --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get the parent TTreeView.
+ *
+ * @return the parent TTreeView
+ */
+ public final TTreeView getTreeView() {
+ return view;
+ }
+
+ /**
+ * Get the displayable text for this item.
+ *
+ * @return the displayable text for this item
+ */
+ public final String getText() {
+ return text;
+ }
+
+ /**
+ * Set the displayable text for this item.
+ *
+ * @param text the displayable text for this item
+ */
+ public final void setText(final String text) {
+ this.text = text;
+ }
+
+ /**
+ * Get expanded value.
+ *
+ * @return if true, this item is expanded
+ */
+ public final boolean isExpanded() {
+ return expanded;
+ }
+
+ /**
+ * Set expanded value.
+ *
+ * @param expanded new value
+ */
+ public final void setExpanded(final boolean expanded) {
+ if (level == 0) {
+ // Root node can't be unexpanded, ever.
+ this.expanded = true;
+ return;
+ }
+ if (level > 0) {
+ this.expanded = expanded;
+ }
+ }
+
+ /**
+ * Get expandable value.
+ *
+ * @return if true, this item is expandable
+ */
+ public final boolean isExpandable() {
+ return expandable;
+ }
+
+ /**
+ * Set expandable value.
+ *
+ * @param expandable new value
+ */
+ public final void setExpandable(final boolean expandable) {
+ if (level == 0) {
+ // Root node can't be unexpanded, ever.
+ this.expandable = true;
+ return;
+ }
+ if (level > 0) {
+ this.expandable = expandable;
+ }
+ }
+
+ /**
+ * Get the vertical bars and such along the left side.
+ *
+ * @return the vertical bars and such along the left side
+ */
+ public final String getPrefix() {
+ return prefix;
+ }
+
+ /**
+ * Get selected value.
+ *
+ * @return if true, this item is selected
+ */
+ public final boolean isSelected() {
+ return selected;
+ }
+
+ /**
+ * Set selected value.
+ *
+ * @param selected new value
+ */
+ public final void setSelected(final boolean selected) {
+ this.selected = selected;
+ }
+
+ /**
+ * Set selectable value.
+ *
+ * @param selectable new value
+ */
+ public final void setSelectable(final boolean selectable) {
+ this.selectable = selectable;
+ }
+
+ /**
+ * Get the length of the widest item to display.
+ *
+ * @return the maximum number of columns for this item or its children
+ */
+ public int getMaximumColumn() {
+ int max = prefix.length() + 4 + StringUtils.width(text);
+ for (TWidget widget: getChildren()) {
+ TTreeItem item = (TTreeItem) widget;
+ int n = item.prefix.length() + 4 + StringUtils.width(item.text);
+ if (n > max) {
+ max = n;
+ }
+ }
+ return max;
+ }
+
+ /**
+ * Recursively expand the tree into a linear array of items.
+ *
+ * @param prefix vertical bar of parent levels and such that is set on
+ * each child
+ * @param last if true, this is the "last" leaf node of a tree
+ * @return additional items to add to the array
+ */
+ public List<TTreeItem> expandTree(final String prefix, final boolean last) {
+ List<TTreeItem> array = new ArrayList<TTreeItem>();
+ this.last = last;
+ this.prefix = prefix;
+ array.add(this);
+
+ if ((getChildren().size() == 0) || !expanded) {
+ return array;
+ }
+
+ String newPrefix = prefix;
+ if (level > 0) {
+ if (last) {
+ newPrefix += " ";
+ } else {
+ newPrefix += GraphicsChars.CP437[0xB3];
+ newPrefix += ' ';
+ }
+ }
+ for (int i = 0; i < getChildren().size(); i++) {
+ TTreeItem item = (TTreeItem) getChildren().get(i);
+ if (i == getChildren().size() - 1) {
+ array.addAll(item.expandTree(newPrefix, true));
+ } else {
+ array.addAll(item.expandTree(newPrefix, false));
+ }
+ }
+ return array;
+ }
+
+ /**
+ * Get the x spot for the + or - to expand/collapse.
+ *
+ * @return column of the expand/collapse button
+ */
+ private int getExpanderX() {
+ if ((level == 0) || (!expandable)) {
+ return 0;
+ }
+ return prefix.length() + 3;
+ }
+
+ /**
+ * Recursively unselect me and my children.
+ */
+ public void unselect() {
+ if (selected == true) {
+ selected = false;
+ view.setSelected(null, false);
+ }
+ for (TWidget widget: getChildren()) {
+ if (widget instanceof TTreeItem) {
+ TTreeItem item = (TTreeItem) widget;
+ item.unselect();
+ }
+ }
+ }
+
+ }
--- /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.ttree;
+
+ import jexer.TAction;
+ import jexer.TKeypress;
+ import jexer.TWidget;
+ import jexer.event.TKeypressEvent;
+ import static jexer.TKeypress.*;
+
+ /**
+ * TTreeView implements a simple tree view.
+ */
+ public class TTreeView extends TWidget {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Root of the tree.
+ */
+ private TTreeItem treeRoot;
+
+ /**
+ * Only one of my children can be selected.
+ */
+ private TTreeItem selectedItem = null;
+
+ /**
+ * The action to perform when the user selects an item.
+ */
+ private TAction action = null;
+
+ /**
+ * The top line currently visible.
+ */
+ private int topLine = 0;
+
+ /**
+ * The left column currently visible.
+ */
+ private int leftColumn = 0;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of tree view
+ * @param height height of tree view
+ */
+ public TTreeView(final TWidget parent, final int x, final int y,
+ final int width, final int height) {
+
+ this(parent, x, y, width, height, null);
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of tree view
+ * @param height height of tree view
+ * @param action action to perform when an item is selected
+ */
+ public TTreeView(final TWidget parent, final int x, final int y,
+ final int width, final int height, final TAction action) {
+
+ super(parent, x, y, width, height);
+ this.action = action;
+ }
+
+ // ------------------------------------------------------------------------
+ // Event handlers ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Handle keystrokes.
+ *
+ * @param keypress keystroke event
+ */
+ @Override
+ public void onKeypress(final TKeypressEvent keypress) {
+ if (keypress.equals(kbUp)) {
+ // Select the previous item
+ if (selectedItem != null) {
+ if (selectedItem.keyboardPrevious != null) {
+ setSelected(selectedItem.keyboardPrevious, true);
+ }
+ }
+ } else if (keypress.equals(kbDown)) {
+ // Select the next item
+ if (selectedItem != null) {
+ if (selectedItem.keyboardNext != null) {
+ setSelected(selectedItem.keyboardNext, true);
+ }
+ }
+ } else if (keypress.equals(kbPgDn)) {
+ for (int i = 0; i < getHeight() - 1; i++) {
+ onKeypress(new TKeypressEvent(TKeypress.kbDown));
+ }
+ } else if (keypress.equals(kbPgUp)) {
+ for (int i = 0; i < getHeight() - 1; i++) {
+ onKeypress(new TKeypressEvent(TKeypress.kbUp));
+ }
+ } else if (keypress.equals(kbHome)) {
+ setSelected((TTreeItem) getChildren().get(0), false);
+ setTopLine(0);
+ } else if (keypress.equals(kbEnd)) {
+ setSelected((TTreeItem) getChildren().get(getChildren().size() - 1),
+ true);
+ } else {
+ if (selectedItem != null) {
+ selectedItem.onKeypress(keypress);
+ } else {
+ // Pass other keys (tab etc.) on to TWidget's handler.
+ super.onKeypress(keypress);
+ }
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // TWidget ----------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+
+ // ------------------------------------------------------------------------
+ // TTreeView --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get the root of the tree.
+ *
+ * @return the root of the tree
+ */
+ public final TTreeItem getTreeRoot() {
+ return treeRoot;
+ }
+
+ /**
+ * Set the root of the tree.
+ *
+ * @param treeRoot the new root of the tree
+ */
+ public final void setTreeRoot(final TTreeItem treeRoot) {
+ this.treeRoot = treeRoot;
+ alignTree();
+ }
+
+ /**
+ * Get the tree view item that was selected.
+ *
+ * @return the selected item, or null if no item is selected
+ */
+ public final TTreeItem getSelected() {
+ return selectedItem;
+ }
+
+ /**
+ * Set the new selected tree view item.
+ *
+ * @param item new item that became selected
+ * @param centerWindow if true, move the window to put the selected into
+ * view
+ */
+ public void setSelected(final TTreeItem item, final boolean centerWindow) {
+ if (item != null) {
+ item.setSelected(true);
+ }
+ if ((selectedItem != null) && (selectedItem != item)) {
+ selectedItem.setSelected(false);
+ }
+ selectedItem = item;
+
+ if (centerWindow) {
+ int y = 0;
+ for (TWidget widget: getChildren()) {
+ if (widget == selectedItem) {
+ break;
+ }
+ y++;
+ }
+ topLine = y - (getHeight() - 1)/2;
+ if (topLine > getChildren().size() - getHeight()) {
+ topLine = getChildren().size() - getHeight();
+ }
+ if (topLine < 0) {
+ topLine = 0;
+ }
+ }
+
+ if (selectedItem != null) {
+ activate(selectedItem);
+ }
+ }
+
+ /**
+ * Perform user selection action.
+ */
+ public void dispatch() {
+ if (action != null) {
+ action.DO(this);
+ }
+ }
+
+ /**
+ * Get the left column value. 0 is the leftmost column.
+ *
+ * @return the left column
+ */
+ public int getLeftColumn() {
+ return leftColumn;
+ }
+
+ /**
+ * Set the left column value. 0 is the leftmost column.
+ *
+ * @param leftColumn the new left column
+ */
+ public void setLeftColumn(final int leftColumn) {
+ this.leftColumn = leftColumn;
+ }
+
+ /**
+ * Get the top line (row) value. 0 is the topmost line.
+ *
+ * @return the top line
+ */
+ public int getTopLine() {
+ return topLine;
+ }
+
+ /**
+ * Set the top line value. 0 is the topmost line.
+ *
+ * @param topLine the new top line
+ */
+ public void setTopLine(final int topLine) {
+ this.topLine = topLine;
+ }
+
+ /**
+ * Get the total line (rows) count, based on the items that are visible
+ * and expanded.
+ *
+ * @return the line count
+ */
+ public int getTotalLineCount() {
+ if (treeRoot == null) {
+ return 0;
+ }
+ return getChildren().size();
+ }
+
+ /**
+ * Get the length of the widest item to display.
+ *
+ * @return the maximum number of columns for this item or its children
+ */
+ public int getMaximumColumn() {
+ if (treeRoot == null) {
+ return 0;
+ }
+ return treeRoot.getMaximumColumn();
+ }
+
+ /**
+ * Update the Y positions of all the children items to match the current
+ * topLine value. Note package private access.
+ */
+ void alignTree() {
+ if (treeRoot == null) {
+ return;
+ }
+
+ // As we walk the list we also adjust next/previous pointers,
+ // resulting in a doubly-linked list but only of the expanded items.
+ TTreeItem p = null;
+
+ for (int i = 0; i < getChildren().size(); i++) {
+ TTreeItem item = (TTreeItem) getChildren().get(i);
+
+ if (p != null) {
+ item.keyboardPrevious = p;
+ p.keyboardNext = item;
+ }
+ p = item;
+
+ item.setY(i - topLine);
+ item.setWidth(getWidth());
+ }
+
+ }
+
+ }
--- /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.ttree;
+
+ import jexer.TAction;
+ import jexer.THScroller;
+ import jexer.TKeypress;
+ import jexer.TScrollableWidget;
+ import jexer.TVScroller;
+ import jexer.TWidget;
+ import jexer.bits.StringUtils;
+ import jexer.event.TKeypressEvent;
+ import jexer.event.TMouseEvent;
+ import jexer.event.TResizeEvent;
+ import static jexer.TKeypress.*;
+
+ /**
+ * TTreeViewWidget wraps a tree view with horizontal and vertical scrollbars.
+ */
+ public class TTreeViewWidget extends TScrollableWidget {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The TTreeView
+ */
+ private TTreeView treeView;
+
+ /**
+ * If true, move the window to put the selected item in view. This
+ * normally only happens once after setting treeRoot.
+ */
+ private boolean centerWindow = false;
+
+ /**
+ * Maximum width of a single line.
+ */
+ private int maxLineWidth;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of tree view
+ * @param height height of tree view
+ */
+ public TTreeViewWidget(final TWidget parent, final int x, final int y,
+ final int width, final int height) {
+
+ this(parent, x, y, width, height, null);
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param parent parent widget
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of tree view
+ * @param height height of tree view
+ * @param action action to perform when an item is selected
+ */
+ public TTreeViewWidget(final TWidget parent, final int x, final int y,
+ final int width, final int height, final TAction action) {
+
+ super(parent, x, y, width, height);
+
+ treeView = new TTreeView(this, 0, 0, getWidth() - 1, getHeight() - 1,
+ action);
+
+ vScroller = new TVScroller(this, getWidth() - 1, 0, getHeight() - 1);
+ hScroller = new THScroller(this, 0, getHeight() - 1, getWidth() - 1);
+
+ }
+
+ // ------------------------------------------------------------------------
+ // Event handlers ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Handle window/screen resize events.
+ *
+ * @param event resize event
+ */
+ @Override
+ public void onResize(final TResizeEvent event) {
+ super.onResize(event);
+
+ if (event.getType() == TResizeEvent.Type.WIDGET) {
+ treeView.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
+ getWidth() - 1, getHeight() - 1));
+ return;
+ } else {
+ super.onResize(event);
+ }
+ }
+
+ /**
+ * Handle mouse press events.
+ *
+ * @param mouse mouse button press event
+ */
+ @Override
+ public void onMouseDown(final TMouseEvent mouse) {
+ if (mouse.isMouseWheelUp()) {
+ verticalDecrement();
+ } else if (mouse.isMouseWheelDown()) {
+ verticalIncrement();
+ } else {
+ // Pass to the TreeView or scrollbars
+ super.onMouseDown(mouse);
+ }
+
+ // Update the view to reflect the new scrollbar positions
+ treeView.setTopLine(getVerticalValue());
+ treeView.setLeftColumn(getHorizontalValue());
+ reflowData();
+ }
+
+ /**
+ * Handle mouse release events.
+ *
+ * @param mouse mouse button release event
+ */
+ @Override
+ public void onMouseUp(final TMouseEvent mouse) {
+ // Pass to the TreeView or scrollbars
+ super.onMouseUp(mouse);
+
+ // Update the view to reflect the new scrollbar positions
+ treeView.setTopLine(getVerticalValue());
+ treeView.setLeftColumn(getHorizontalValue());
+ reflowData();
+ }
+
+ /**
+ * Handle mouse motion events.
+ *
+ * @param mouse mouse motion event
+ */
+ @Override
+ public void onMouseMotion(final TMouseEvent mouse) {
+ // Pass to the TreeView or scrollbars
+ super.onMouseMotion(mouse);
+
+ // Update the view to reflect the new scrollbar positions
+ treeView.setTopLine(getVerticalValue());
+ treeView.setLeftColumn(getHorizontalValue());
+ reflowData();
+ }
+
+ /**
+ * Handle keystrokes.
+ *
+ * @param keypress keystroke event
+ */
+ @Override
+ public void onKeypress(final TKeypressEvent keypress) {
+ if (keypress.equals(kbShiftLeft)
+ || keypress.equals(kbCtrlLeft)
+ || keypress.equals(kbAltLeft)
+ ) {
+ horizontalDecrement();
+ } else if (keypress.equals(kbShiftRight)
+ || keypress.equals(kbCtrlRight)
+ || keypress.equals(kbAltRight)
+ ) {
+ horizontalIncrement();
+ } else if (keypress.equals(kbShiftUp)
+ || keypress.equals(kbCtrlUp)
+ || keypress.equals(kbAltUp)
+ ) {
+ verticalDecrement();
+ } else if (keypress.equals(kbShiftDown)
+ || keypress.equals(kbCtrlDown)
+ || keypress.equals(kbAltDown)
+ ) {
+ verticalIncrement();
+ } else if (keypress.equals(kbShiftPgUp)
+ || keypress.equals(kbCtrlPgUp)
+ || keypress.equals(kbAltPgUp)
+ ) {
+ bigVerticalDecrement();
+ } else if (keypress.equals(kbShiftPgDn)
+ || keypress.equals(kbCtrlPgDn)
+ || keypress.equals(kbAltPgDn)
+ ) {
+ bigVerticalIncrement();
+ } else if (keypress.equals(kbPgDn)) {
+ for (int i = 0; i < getHeight() - 2; i++) {
+ treeView.onKeypress(new TKeypressEvent(TKeypress.kbDown));
+ }
+ reflowData();
+ return;
+ } else if (keypress.equals(kbPgUp)) {
+ for (int i = 0; i < getHeight() - 2; i++) {
+ treeView.onKeypress(new TKeypressEvent(TKeypress.kbUp));
+ }
+ reflowData();
+ return;
+ } else if (keypress.equals(kbHome)) {
+ treeView.setSelected((TTreeItem) treeView.getChildren().get(0),
+ false);
+ treeView.setTopLine(0);
+ reflowData();
+ return;
+ } else if (keypress.equals(kbEnd)) {
+ treeView.setSelected((TTreeItem) treeView.getChildren().get(
+ treeView.getChildren().size() - 1), true);
+ reflowData();
+ return;
+ } else if (keypress.equals(kbTab)) {
+ getParent().switchWidget(true);
+ return;
+ } else if (keypress.equals(kbShiftTab)
+ || keypress.equals(kbBackTab)) {
+ getParent().switchWidget(false);
+ return;
+ } else {
+ treeView.onKeypress(keypress);
+
+ // Update the scrollbars to reflect the new data position
+ reflowData();
+ return;
+ }
+
+ // Update the view to reflect the new scrollbar position
+ treeView.setTopLine(getVerticalValue());
+ treeView.setLeftColumn(getHorizontalValue());
+ reflowData();
+ }
+
+ // ------------------------------------------------------------------------
+ // TScrollableWidget ------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Resize text and scrollbars for a new width/height.
+ */
+ @Override
+ public void reflowData() {
+ int selectedRow = 0;
+ boolean foundSelectedRow = false;
+
+ // Reset the keyboard list, expandTree() will recreate it.
+ for (TWidget widget: treeView.getChildren()) {
+ TTreeItem item = (TTreeItem) widget;
+ item.keyboardPrevious = null;
+ item.keyboardNext = null;
+ }
+
+ // Expand the tree into a linear list
+ treeView.getChildren().clear();
+ treeView.getChildren().addAll(treeView.getTreeRoot().expandTree("",
+ true));
+
+ // Locate the selected row and maximum line width
+ for (TWidget widget: treeView.getChildren()) {
+ TTreeItem item = (TTreeItem) widget;
+
+ if (item == treeView.getSelected()) {
+ foundSelectedRow = true;
+ }
+ if (!foundSelectedRow) {
+ selectedRow++;
+ }
+
+ int lineWidth = StringUtils.width(item.getText())
+ + item.getPrefix().length() + 4;
+ if (lineWidth > maxLineWidth) {
+ maxLineWidth = lineWidth;
+ }
+ }
+
+ if ((centerWindow) && (foundSelectedRow)) {
+ if ((selectedRow < getVerticalValue())
+ || (selectedRow > getVerticalValue() + getHeight() - 2)
+ ) {
+ treeView.setTopLine(selectedRow);
+ centerWindow = false;
+ }
+ }
+ treeView.alignTree();
+
+ // Rescale the scroll bars
+ setVerticalValue(treeView.getTopLine());
+ setBottomValue(treeView.getTotalLineCount() - (getHeight() - 1));
+ if (getBottomValue() < getTopValue()) {
+ setBottomValue(getTopValue());
+ }
+ if (getVerticalValue() > getBottomValue()) {
+ setVerticalValue(getBottomValue());
+ }
+ setRightValue(maxLineWidth - 2);
+ if (getHorizontalValue() > getRightValue()) {
+ setHorizontalValue(getRightValue());
+ }
+
+ }
+
+ // ------------------------------------------------------------------------
+ // TTreeView --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get the underlying TTreeView.
+ *
+ * @return the TTreeView
+ */
+ public TTreeView getTreeView() {
+ return treeView;
+ }
+
+ /**
+ * Get the root of the tree.
+ *
+ * @return the root of the tree
+ */
+ public final TTreeItem getTreeRoot() {
+ return treeView.getTreeRoot();
+ }
+
+ /**
+ * Set the root of the tree.
+ *
+ * @param treeRoot the new root of the tree
+ */
+ public final void setTreeRoot(final TTreeItem treeRoot) {
+ treeView.setTreeRoot(treeRoot);
+ }
+
+ /**
+ * Set treeRoot.
+ *
+ * @param treeRoot ultimate root of tree
+ * @param centerWindow if true, move the window to put the root in view
+ */
+ public void setTreeRoot(final TTreeItem treeRoot,
+ final boolean centerWindow) {
+
+ treeView.setTreeRoot(treeRoot);
+ this.centerWindow = centerWindow;
+ }
+
+ /**
+ * Get the tree view item that was selected.
+ *
+ * @return the selected item, or null if no item is selected
+ */
+ public final TTreeItem getSelected() {
+ return treeView.getSelected();
+ }
+
+ /**
+ * Set the new selected tree view item.
+ *
+ * @param item new item that became selected
+ * @param centerWindow if true, move the window to put the selected into
+ * view
+ */
+ public void setSelected(final TTreeItem item, final boolean centerWindow) {
+ treeView.setSelected(item, centerWindow);
+ }
+
+ /**
+ * Perform user selection action.
+ */
+ public void dispatch() {
+ treeView.dispatch();
+ }
+
+ }
--- /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.ttree;
+
+ import jexer.TAction;
+ import jexer.TApplication;
+ import jexer.THScroller;
+ import jexer.TScrollableWindow;
+ import jexer.TVScroller;
+ import jexer.TWidget;
+ import jexer.bits.StringUtils;
+ import jexer.event.TKeypressEvent;
+ import jexer.event.TMouseEvent;
+ import jexer.event.TResizeEvent;
+ import static jexer.TKeypress.*;
+
+ /**
+ * TTreeViewWindow wraps a tree view with horizontal and vertical scrollbars
+ * in a standalone window.
+ */
+ public class TTreeViewWindow extends TScrollableWindow {
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The TTreeView
+ */
+ private TTreeView treeView;
+
+ /**
+ * If true, move the window to put the selected item in view. This
+ * normally only happens once after setting treeRoot.
+ */
+ private boolean centerWindow = false;
+
+ /**
+ * Maximum width of a single line.
+ */
+ private int maxLineWidth;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param parent the main application
+ * @param title the window title
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of tree view
+ * @param flags bitmask of RESIZABLE, CENTERED, or MODAL
+ * @param height height of tree view
+ */
+ public TTreeViewWindow(final TApplication parent, final String title,
+ final int x, final int y, final int width, final int height,
+ final int flags) {
+
+ this(parent, title, x, y, width, height, flags, null);
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param parent the main application
+ * @param title the window title
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param width width of tree view
+ * @param height height of tree view
+ * @param flags bitmask of RESIZABLE, CENTERED, or MODAL
+ * @param action action to perform when an item is selected
+ */
+ public TTreeViewWindow(final TApplication parent, final String title,
+ final int x, final int y, final int width, final int height,
+ final int flags, final TAction action) {
+
+ super(parent, title, x, y, width, height, flags);
+
+ treeView = new TTreeView(this, 0, 0, getWidth() - 2, getHeight() - 2,
+ action);
+
+ hScroller = new THScroller(this, 17, getHeight() - 2, getWidth() - 20);
+ vScroller = new TVScroller(this, getWidth() - 2, 0, getHeight() - 2);
+
+ /*
+ System.err.println("TTreeViewWindow()");
+ for (TWidget w: getChildren()) {
+ System.err.println(" " + w + " " + w.isActive());
+ }
+ */
+ }
+
+ // ------------------------------------------------------------------------
+ // Event handlers ---------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Handle mouse press events.
+ *
+ * @param mouse mouse button press event
+ */
+ @Override
+ public void onMouseDown(final TMouseEvent mouse) {
+ if (mouse.isMouseWheelUp()) {
+ verticalDecrement();
+ } else if (mouse.isMouseWheelDown()) {
+ verticalIncrement();
+ } else {
+ // Pass to the TreeView or scrollbars
+ super.onMouseDown(mouse);
+ }
+
+ // Update the view to reflect the new scrollbar positions
+ treeView.setTopLine(getVerticalValue());
+ treeView.setLeftColumn(getHorizontalValue());
+ reflowData();
+ }
+
+ /**
+ * Handle mouse release events.
+ *
+ * @param mouse mouse button release event
+ */
+ @Override
+ public void onMouseUp(final TMouseEvent mouse) {
+ // Pass to the TreeView or scrollbars
+ super.onMouseUp(mouse);
+
+ // Update the view to reflect the new scrollbar positions
+ treeView.setTopLine(getVerticalValue());
+ treeView.setLeftColumn(getHorizontalValue());
+ reflowData();
+ }
+
+ /**
+ * Handle mouse motion events.
+ *
+ * @param mouse mouse motion event
+ */
+ @Override
+ public void onMouseMotion(final TMouseEvent mouse) {
+ // Pass to the TreeView or scrollbars
+ super.onMouseMotion(mouse);
+
+ // Update the view to reflect the new scrollbar positions
+ treeView.setTopLine(getVerticalValue());
+ treeView.setLeftColumn(getHorizontalValue());
+ reflowData();
+ }
+
+ /**
+ * Handle keystrokes.
+ *
+ * @param keypress keystroke event
+ */
+ @Override
+ public void onKeypress(final TKeypressEvent keypress) {
+ if (inKeyboardResize) {
+ // Let TWindow do its job.
+ super.onKeypress(keypress);
+ return;
+ }
+
+ // Give the shortcut bar a shot at this.
+ if (statusBar != null) {
+ if (statusBar.statusBarKeypress(keypress)) {
+ return;
+ }
+ }
+
+ if (keypress.equals(kbShiftLeft)
+ || keypress.equals(kbCtrlLeft)
+ || keypress.equals(kbAltLeft)
+ ) {
+ horizontalDecrement();
+ } else if (keypress.equals(kbShiftRight)
+ || keypress.equals(kbCtrlRight)
+ || keypress.equals(kbAltRight)
+ ) {
+ horizontalIncrement();
+ } else if (keypress.equals(kbShiftUp)
+ || keypress.equals(kbCtrlUp)
+ || keypress.equals(kbAltUp)
+ ) {
+ verticalDecrement();
+ } else if (keypress.equals(kbShiftDown)
+ || keypress.equals(kbCtrlDown)
+ || keypress.equals(kbAltDown)
+ ) {
+ verticalIncrement();
+ } else if (keypress.equals(kbShiftPgUp)
+ || keypress.equals(kbCtrlPgUp)
+ || keypress.equals(kbAltPgUp)
+ ) {
+ bigVerticalDecrement();
+ } else if (keypress.equals(kbShiftPgDn)
+ || keypress.equals(kbCtrlPgDn)
+ || keypress.equals(kbAltPgDn)
+ ) {
+ bigVerticalIncrement();
+ } else {
+ treeView.onKeypress(keypress);
+
+ // Update the scrollbars to reflect the new data position
+ reflowData();
+ return;
+ }
+
+ // Update the view to reflect the new scrollbar position
+ treeView.setTopLine(getVerticalValue());
+ treeView.setLeftColumn(getHorizontalValue());
+ reflowData();
+ }
+
+ // ------------------------------------------------------------------------
+ // TScrollableWindow ------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Handle window/screen resize events.
+ *
+ * @param resize resize event
+ */
+ @Override
+ public void onResize(final TResizeEvent resize) {
+ if (resize.getType() == TResizeEvent.Type.WIDGET) {
+ // Resize the treeView field.
+ TResizeEvent treeSize = new TResizeEvent(TResizeEvent.Type.WIDGET,
+ resize.getWidth() - 2, resize.getHeight() - 2);
+ treeView.onResize(treeSize);
+
+ // Have TScrollableWindow handle the scrollbars.
+ super.onResize(resize);
+
+ // Now re-center the treeView field.
+ if (treeView.getSelected() != null) {
+ treeView.setSelected(treeView.getSelected(), true);
+ }
+ reflowData();
+ return;
+ }
+ }
+
+ /**
+ * Resize text and scrollbars for a new width/height.
+ */
+ @Override
+ public void reflowData() {
+ int selectedRow = 0;
+ boolean foundSelectedRow = false;
+
+ // Reset the keyboard list, expandTree() will recreate it.
+ for (TWidget widget: treeView.getChildren()) {
+ TTreeItem item = (TTreeItem) widget;
+ item.keyboardPrevious = null;
+ item.keyboardNext = null;
+ }
+
+ // Expand the tree into a linear list
+ treeView.getChildren().clear();
+ treeView.getChildren().addAll(treeView.getTreeRoot().expandTree("",
+ true));
+
+ // Locate the selected row and maximum line width
+ for (TWidget widget: treeView.getChildren()) {
+ TTreeItem item = (TTreeItem) widget;
+
+ if (item == treeView.getSelected()) {
+ foundSelectedRow = true;
+ }
+ if (!foundSelectedRow) {
+ selectedRow++;
+ }
+
+ int lineWidth = StringUtils.width(item.getText())
+ + item.getPrefix().length() + 4;
+ if (lineWidth > maxLineWidth) {
+ maxLineWidth = lineWidth;
+ }
+ }
+
+ if ((centerWindow) && (foundSelectedRow)) {
+ if ((selectedRow < getVerticalValue())
+ || (selectedRow > getVerticalValue() + getHeight() - 3)
+ ) {
+ treeView.setTopLine(selectedRow);
+ centerWindow = false;
+ }
+ }
+ treeView.alignTree();
+
+ // Rescale the scroll bars
+ setVerticalValue(treeView.getTopLine());
+ setBottomValue(treeView.getTotalLineCount() - (getHeight() - 2));
+ if (getBottomValue() < getTopValue()) {
+ setBottomValue(getTopValue());
+ }
+ if (getVerticalValue() > getBottomValue()) {
+ setVerticalValue(getBottomValue());
+ }
+ setRightValue(maxLineWidth - 4);
+ if (getHorizontalValue() > getRightValue()) {
+ setHorizontalValue(getRightValue());
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // TTreeView --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get the underlying TTreeView.
+ *
+ * @return the TTreeView
+ */
+ public TTreeView getTreeView() {
+ return treeView;
+ }
+
+ /**
+ * Get the root of the tree.
+ *
+ * @return the root of the tree
+ */
+ public final TTreeItem getTreeRoot() {
+ return treeView.getTreeRoot();
+ }
+
+ /**
+ * Set the root of the tree.
+ *
+ * @param treeRoot the new root of the tree
+ */
+ public final void setTreeRoot(final TTreeItem treeRoot) {
+ treeView.setTreeRoot(treeRoot);
+ }
+
+ /**
+ * Set treeRoot.
+ *
+ * @param treeRoot ultimate root of tree
+ * @param centerWindow if true, move the window to put the root in view
+ */
+ public void setTreeRoot(final TTreeItem treeRoot,
+ final boolean centerWindow) {
+
+ treeView.setTreeRoot(treeRoot);
+ this.centerWindow = centerWindow;
+ }
+
+ /**
+ * Get the tree view item that was selected.
+ *
+ * @return the selected item, or null if no item is selected
+ */
+ public final TTreeItem getSelected() {
+ return treeView.getSelected();
+ }
+
+ /**
+ * Set the new selected tree view item.
+ *
+ * @param item new item that became selected
+ * @param centerWindow if true, move the window to put the selected into
+ * view
+ */
+ public void setSelected(final TTreeItem item, final boolean centerWindow) {
+ treeView.setSelected(item, centerWindow);
+ }
+
+ /**
+ * Perform user selection action.
+ */
+ public void dispatch() {
+ treeView.dispatch();
+ }
+
+ }
--- /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
+ */
+
+ /**
+ * TTreeView and supporting classes.
+ */
+ package jexer.ttree;