From: Niki Roo Date: Thu, 24 Oct 2019 07:15:15 +0000 (+0200) Subject: Add 'src/jexer/' from commit 'cf01c92f5809a0732409e280fb0f32f27393618d' X-Git-Url: http://git.nikiroo.be/?p=fanfix.git;a=commitdiff_plain;h=12b90437b5f22c2ae6e9b9b14c3b62b60f6143e5;hp=b709b36e17eb8807819e51297bb398ef28ece52d Add 'src/jexer/' from commit 'cf01c92f5809a0732409e280fb0f32f27393618d' git-subtree-dir: src/jexer git-subtree-mainline: b709b36e17eb8807819e51297bb398ef28ece52d git-subtree-split: cf01c92f5809a0732409e280fb0f32f27393618d --- diff --git a/src/jexer/.classpath b/src/jexer/.classpath new file mode 100644 index 0000000..9b07da8 --- /dev/null +++ b/src/jexer/.classpath @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/jexer/.gitignore b/src/jexer/.gitignore new file mode 100644 index 0000000..30d9f7c --- /dev/null +++ b/src/jexer/.gitignore @@ -0,0 +1,35 @@ +*.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/** diff --git a/src/jexer/.project b/src/jexer/.project new file mode 100644 index 0000000..c0afd85 --- /dev/null +++ b/src/jexer/.project @@ -0,0 +1,17 @@ + + + jexer + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/src/jexer/Scrollable.java b/src/jexer/Scrollable.java new file mode 100644 index 0000000..b844ca6 --- /dev/null +++ b/src/jexer/Scrollable.java @@ -0,0 +1,280 @@ +/* + * 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(); + +} diff --git a/src/jexer/TAction.java b/src/jexer/TAction.java new file mode 100644 index 0000000..5343143 --- /dev/null +++ b/src/jexer/TAction.java @@ -0,0 +1,75 @@ +/* + * 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(); +} diff --git a/src/jexer/TApplication.java b/src/jexer/TApplication.java new file mode 100644 index 0000000..9d27c10 --- /dev/null +++ b/src/jexer/TApplication.java @@ -0,0 +1,3818 @@ +/* + * 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 fillEventQueue; + + /** + * Event queue that will be drained by either primary or secondary + * Thread. + */ + private List drainEventQueue; + + /** + * Top-level menus in this application. + */ + private List menus; + + /** + * Stack of activated sub-menus in this application. + */ + private List subMenus; + + /** + * The currently active menu. + */ + private TMenu activeMenu = null; + + /** + * Active keyboard accelerators. + */ + private Map accelerators; + + /** + * All menu items. + */ + private List menuItems; + + /** + * Windows and widgets pull colors from this ColorTheme. + */ + private ColorTheme theme; + + /** + * The top-level windows (but not menus). + */ + private List windows; + + /** + * The currently acive window. + */ + private TWindow activeWindow = null; + + /** + * Timers that are being ticked. + */ + private List 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 invokeLaters = new LinkedList(); + + /** + * 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(); + drainEventQueue = new LinkedList(); + windows = new LinkedList(); + menus = new ArrayList(); + subMenus = new ArrayList(); + timers = new LinkedList(); + accelerators = new HashMap(); + menuItems = new LinkedList(); + 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 keepTimers = new LinkedList(); + 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 getAllWindows() { + List result = new ArrayList(); + 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 filters = new ArrayList(); + filters.add("^.*\\.[Jj][Pp][Gg]$"); + filters.add("^.*\\.[Jj][Pp][Ee][Gg]$"); + filters.add("^.*\\.[Pp][Nn][Gg]$"); + filters.add("^.*\\.[Gg][Ii][Ff]$"); + filters.add("^.*\\.[Bb][Mm][Pp]$"); + 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 sorted = new ArrayList(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 sorted = new ArrayList(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 sorted = new ArrayList(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 menus = new ArrayList(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 getAllMenus() { + return new ArrayList(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 filters = new ArrayList(); + 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 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; + } + +} diff --git a/src/jexer/TApplication.properties b/src/jexer/TApplication.properties new file mode 100644 index 0000000..299c6a3 --- /dev/null +++ b/src/jexer/TApplication.properties @@ -0,0 +1,27 @@ +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} diff --git a/src/jexer/TButton.java b/src/jexer/TButton.java new file mode 100644 index 0000000..d86fa44 --- /dev/null +++ b/src/jexer/TButton.java @@ -0,0 +1,333 @@ +/* + * 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; + } + } + +} diff --git a/src/jexer/TCalendar.java b/src/jexer/TCalendar.java new file mode 100644 index 0000000..c2005cc --- /dev/null +++ b/src/jexer/TCalendar.java @@ -0,0 +1,324 @@ +/* + * 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); + } + +} diff --git a/src/jexer/TCheckBox.java b/src/jexer/TCheckBox.java new file mode 100644 index 0000000..1f9a351 --- /dev/null +++ b/src/jexer/TCheckBox.java @@ -0,0 +1,216 @@ +/* + * 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; + } + +} diff --git a/src/jexer/TComboBox.java b/src/jexer/TComboBox.java new file mode 100644 index 0000000..1164e6c --- /dev/null +++ b/src/jexer/TComboBox.java @@ -0,0 +1,465 @@ +/* + * 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 values = new ArrayList(); + + /** + * 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 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 getList() { + return list.getList(); + } + + /** + * Set the new list of strings to display. + * + * @param list new list of strings + */ + public final void setList(final List 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; + } + } +} diff --git a/src/jexer/TCommand.java b/src/jexer/TCommand.java new file mode 100644 index 0000000..874a29d --- /dev/null +++ b/src/jexer/TCommand.java @@ -0,0 +1,227 @@ +/* + * 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; + } + +} diff --git a/src/jexer/TDesktop.java b/src/jexer/TDesktop.java new file mode 100644 index 0000000..5aa52af --- /dev/null +++ b/src/jexer/TDesktop.java @@ -0,0 +1,258 @@ +/* + * 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. + * + *

+ * Events are passed to TDesktop as follows: + *

    + *
  • Mouse events are seen if they do not cover any other windows.
  • + *
  • Keypress events are seen if no other windows are open.
  • + *
  • Menu events are seen if no other windows are open.
  • + *
  • Command events are seen if no other windows are open.
  • + *
+ */ +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; + } + +} diff --git a/src/jexer/TDirectoryList.java b/src/jexer/TDirectoryList.java new file mode 100644 index 0000000..322ff5c --- /dev/null +++ b/src/jexer/TDirectoryList.java @@ -0,0 +1,234 @@ +/* + * 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 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 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 filters) { + + super(parent, null, x, y, width, height, action); + files = new HashMap(); + 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 newStrings = new ArrayList(); + 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)); + } + +} diff --git a/src/jexer/TEditColorThemeWindow.java b/src/jexer/TEditColorThemeWindow.java new file mode 100644 index 0000000..668309d --- /dev/null +++ b/src/jexer/TEditColorThemeWindow.java @@ -0,0 +1,789 @@ +/* + * 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 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 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); + } + +} diff --git a/src/jexer/TEditColorThemeWindow.properties b/src/jexer/TEditColorThemeWindow.properties new file mode 100644 index 0000000..f4c6220 --- /dev/null +++ b/src/jexer/TEditColorThemeWindow.properties @@ -0,0 +1,8 @@ +foregroundLabel=\ Foreground\ +backgroundLabel=\ Background\ +windowTitle=Colors +okButton=\ \ &OK\ \ +cancelButton=&Cancel +statusBar=Select Colors +colorName=Color Name +textTextText=Text Text Text diff --git a/src/jexer/TEditorWidget.java b/src/jexer/TEditorWidget.java new file mode 100644 index 0000000..a694533 --- /dev/null +++ b/src/jexer/TEditorWidget.java @@ -0,0 +1,546 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2019 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ +package jexer; + +import java.io.IOException; + +import jexer.bits.CellAttributes; +import jexer.event.TKeypressEvent; +import jexer.event.TMouseEvent; +import jexer.event.TResizeEvent; +import jexer.teditor.Document; +import jexer.teditor.Line; +import jexer.teditor.Word; +import static jexer.TKeypress.*; + +/** + * TEditorWidget displays an editable text document. It is unaware of + * scrolling behavior, but can respond to mouse and keyboard events. + */ +public class TEditorWidget extends TWidget { + + // ------------------------------------------------------------------------ + // Constants -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * The number of lines to scroll on mouse wheel up/down. + */ + private static final int wheelScrollSize = 3; + + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * The document being edited. + */ + private Document document; + + /** + * The default color for the TEditor class. + */ + private CellAttributes defaultColor = null; + + /** + * The topmost line number in the visible area. 0-based. + */ + private int topLine = 0; + + /** + * The leftmost column number in the visible area. 0-based. + */ + private int leftColumn = 0; + + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Public constructor. + * + * @param parent parent widget + * @param text text on the screen + * @param x column relative to parent + * @param y row relative to parent + * @param width width of text area + * @param height height of text area + */ + public TEditorWidget(final TWidget parent, final String text, final int x, + final int y, final int width, final int height) { + + // Set parent and window + super(parent, x, y, width, height); + + setCursorVisible(true); + + defaultColor = getTheme().getColor("teditor"); + document = new Document(text, defaultColor); + } + + // ------------------------------------------------------------------------ + // TWidget ---------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Draw the text box. + */ + @Override + public void draw() { + for (int i = 0; i < getHeight(); i++) { + // Background line + getScreen().hLineXY(0, i, getWidth(), ' ', defaultColor); + + // Now draw document's line + if (topLine + i < document.getLineCount()) { + Line line = document.getLine(topLine + i); + int x = 0; + for (Word word: line.getWords()) { + // For now, we are cheating: draw outside the left region + // if needed and let screen do the clipping. + getScreen().putStringXY(x - leftColumn, i, word.getText(), + word.getColor()); + x += word.getDisplayLength(); + if (x - leftColumn > getWidth()) { + break; + } + } + } + } + } + + /** + * Handle mouse press events. + * + * @param mouse mouse button press event + */ + @Override + public void onMouseDown(final TMouseEvent mouse) { + if (mouse.isMouseWheelUp()) { + for (int i = 0; i < wheelScrollSize; i++) { + if (topLine > 0) { + topLine--; + alignDocument(false); + } + } + return; + } + if (mouse.isMouseWheelDown()) { + for (int i = 0; i < wheelScrollSize; i++) { + if (topLine < document.getLineCount() - 1) { + topLine++; + alignDocument(true); + } + } + return; + } + + if (mouse.isMouse1()) { + // Set the row and column + int newLine = topLine + mouse.getY(); + int newX = leftColumn + mouse.getX(); + if (newLine > document.getLineCount() - 1) { + // Go to the end + document.setLineNumber(document.getLineCount() - 1); + document.end(); + if (newLine > document.getLineCount() - 1) { + setCursorY(document.getLineCount() - 1 - topLine); + } else { + setCursorY(mouse.getY()); + } + alignCursor(); + return; + } + + document.setLineNumber(newLine); + setCursorY(mouse.getY()); + if (newX >= document.getCurrentLine().getDisplayLength()) { + document.end(); + alignCursor(); + } else { + document.setCursor(newX); + setCursorX(mouse.getX()); + } + return; + } + + // Pass to children + super.onMouseDown(mouse); + } + + /** + * Handle keystrokes. + * + * @param keypress keystroke event + */ + @Override + public void onKeypress(final TKeypressEvent keypress) { + if (keypress.equals(kbLeft)) { + document.left(); + alignTopLine(false); + } else if (keypress.equals(kbRight)) { + document.right(); + alignTopLine(true); + } else if (keypress.equals(kbAltLeft) + || keypress.equals(kbCtrlLeft) + ) { + document.backwardsWord(); + alignTopLine(false); + } else if (keypress.equals(kbAltRight) + || keypress.equals(kbCtrlRight) + ) { + document.forwardsWord(); + alignTopLine(true); + } else if (keypress.equals(kbUp)) { + document.up(); + alignTopLine(false); + } else if (keypress.equals(kbDown)) { + document.down(); + alignTopLine(true); + } else if (keypress.equals(kbPgUp)) { + document.up(getHeight() - 1); + alignTopLine(false); + } else if (keypress.equals(kbPgDn)) { + document.down(getHeight() - 1); + alignTopLine(true); + } else if (keypress.equals(kbHome)) { + if (document.home()) { + leftColumn = 0; + if (leftColumn < 0) { + leftColumn = 0; + } + setCursorX(0); + } + } else if (keypress.equals(kbEnd)) { + if (document.end()) { + alignCursor(); + } + } else if (keypress.equals(kbCtrlHome)) { + document.setLineNumber(0); + document.home(); + topLine = 0; + leftColumn = 0; + setCursorX(0); + setCursorY(0); + } else if (keypress.equals(kbCtrlEnd)) { + document.setLineNumber(document.getLineCount() - 1); + document.end(); + alignTopLine(false); + } else if (keypress.equals(kbIns)) { + document.setOverwrite(!document.getOverwrite()); + } else if (keypress.equals(kbDel)) { + document.del(); + alignCursor(); + } else if (keypress.equals(kbBackspace) + || keypress.equals(kbBackspaceDel) + ) { + document.backspace(); + alignTopLine(false); + } else if (keypress.equals(kbTab)) { + // TODO: tab character. For now just add spaces until we hit + // modulo 8. + for (int i = document.getCursor(); (i + 1) % 8 != 0; i++) { + document.addChar(' '); + } + alignCursor(); + } else if (keypress.equals(kbEnter)) { + document.enter(); + alignTopLine(true); + } else if (!keypress.getKey().isFnKey() + && !keypress.getKey().isAlt() + && !keypress.getKey().isCtrl() + ) { + // Plain old keystroke, process it + document.addChar(keypress.getKey().getChar()); + alignCursor(); + } else { + // Pass other keys (tab etc.) on to TWidget + super.onKeypress(keypress); + } + } + + /** + * Method that subclasses can override to handle window/screen resize + * events. + * + * @param resize resize event + */ + @Override + public void onResize(final TResizeEvent resize) { + // Change my width/height, and pull the cursor in as needed. + if (resize.getType() == TResizeEvent.Type.WIDGET) { + setWidth(resize.getWidth()); + setHeight(resize.getHeight()); + // See if the cursor is now outside the window, and if so move + // things. + if (getCursorX() >= getWidth()) { + leftColumn += getCursorX() - (getWidth() - 1); + setCursorX(getWidth() - 1); + } + if (getCursorY() >= getHeight()) { + topLine += getCursorY() - (getHeight() - 1); + setCursorY(getHeight() - 1); + } + } else { + // Let superclass handle it + super.onResize(resize); + } + } + + // ------------------------------------------------------------------------ + // TEditorWidget ---------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Align visible area with document current line. + * + * @param topLineIsTop if true, make the top visible line the document + * current line if it was off-screen. If false, make the bottom visible + * line the document current line. + */ + private void alignTopLine(final boolean topLineIsTop) { + int line = document.getLineNumber(); + + if ((line < topLine) || (line > topLine + getHeight() - 1)) { + // Need to move topLine to bring document back into view. + if (topLineIsTop) { + topLine = line - (getHeight() - 1); + if (topLine < 0) { + topLine = 0; + } + assert (topLine >= 0); + } else { + topLine = line; + assert (topLine >= 0); + } + } + + /* + System.err.println("line " + line + " topLine " + topLine); + */ + + // Document is in view, let's set cursorY + assert (line >= topLine); + setCursorY(line - topLine); + alignCursor(); + } + + /** + * Align document current line with visible area. + * + * @param topLineIsTop if true, make the top visible line the document + * current line if it was off-screen. If false, make the bottom visible + * line the document current line. + */ + private void alignDocument(final boolean topLineIsTop) { + int line = document.getLineNumber(); + int cursor = document.getCursor(); + + if ((line < topLine) || (line > topLine + getHeight() - 1)) { + // Need to move document to ensure it fits view. + if (topLineIsTop) { + document.setLineNumber(topLine); + } else { + document.setLineNumber(topLine + (getHeight() - 1)); + } + if (cursor < document.getCurrentLine().getDisplayLength()) { + document.setCursor(cursor); + } + } + + /* + System.err.println("getLineNumber() " + document.getLineNumber() + + " topLine " + topLine); + */ + + // Document is in view, let's set cursorY + setCursorY(document.getLineNumber() - topLine); + alignCursor(); + } + + /** + * Align visible cursor with document cursor. + */ + private void alignCursor() { + int width = getWidth(); + + int desiredX = document.getCursor() - leftColumn; + if (desiredX < 0) { + // We need to push the screen to the left. + leftColumn = document.getCursor(); + } else if (desiredX > width - 1) { + // We need to push the screen to the right. + leftColumn = document.getCursor() - (width - 1); + } + + /* + System.err.println("document cursor " + document.getCursor() + + " leftColumn " + leftColumn); + */ + + + setCursorX(document.getCursor() - leftColumn); + } + + /** + * Get the number of lines in the underlying Document. + * + * @return the number of lines + */ + public int getLineCount() { + return document.getLineCount(); + } + + /** + * Get the current visible top row number. 1-based. + * + * @return the visible top row number. Row 1 is the first row. + */ + public int getVisibleRowNumber() { + return topLine + 1; + } + + /** + * Set the current visible row number. 1-based. + * + * @param row the new visible row number. Row 1 is the first row. + */ + public void setVisibleRowNumber(final int row) { + assert (row > 0); + if ((row > 0) && (row < document.getLineCount())) { + topLine = row - 1; + alignDocument(true); + } + } + + /** + * Get the current editing row number. 1-based. + * + * @return the editing row number. Row 1 is the first row. + */ + public int getEditingRowNumber() { + return document.getLineNumber() + 1; + } + + /** + * Set the current editing row number. 1-based. + * + * @param row the new editing row number. Row 1 is the first row. + */ + public void setEditingRowNumber(final int row) { + assert (row > 0); + if ((row > 0) && (row < document.getLineCount())) { + document.setLineNumber(row - 1); + alignTopLine(true); + } + } + + /** + * Set the current visible column number. 1-based. + * + * @return the visible column number. Column 1 is the first column. + */ + public int getVisibleColumnNumber() { + return leftColumn + 1; + } + + /** + * Set the current visible column number. 1-based. + * + * @param column the new visible column number. Column 1 is the first + * column. + */ + public void setVisibleColumnNumber(final int column) { + assert (column > 0); + if ((column > 0) && (column < document.getLineLengthMax())) { + leftColumn = column - 1; + alignDocument(true); + } + } + + /** + * Get the current editing column number. 1-based. + * + * @return the editing column number. Column 1 is the first column. + */ + public int getEditingColumnNumber() { + return document.getCursor() + 1; + } + + /** + * Set the current editing column number. 1-based. + * + * @param column the new editing column number. Column 1 is the first + * column. + */ + public void setEditingColumnNumber(final int column) { + if ((column > 0) && (column < document.getLineLength())) { + document.setCursor(column - 1); + alignCursor(); + } + } + + /** + * Get the maximum possible row number. 1-based. + * + * @return the maximum row number. Row 1 is the first row. + */ + public int getMaximumRowNumber() { + return document.getLineCount() + 1; + } + + /** + * Get the maximum possible column number. 1-based. + * + * @return the maximum column number. Column 1 is the first column. + */ + public int getMaximumColumnNumber() { + return document.getLineLengthMax() + 1; + } + + /** + * Get the dirty value. + * + * @return true if the buffer is dirty + */ + public boolean isDirty() { + return document.isDirty(); + } + + /** + * Save contents to file. + * + * @param filename file to save to + * @throws IOException if a java.io operation throws + */ + public void saveToFilename(final String filename) throws IOException { + document.saveToFilename(filename); + } + +} diff --git a/src/jexer/TEditorWindow.java b/src/jexer/TEditorWindow.java new file mode 100644 index 0000000..d78185c --- /dev/null +++ b/src/jexer/TEditorWindow.java @@ -0,0 +1,452 @@ +/* + * 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; + } + +} diff --git a/src/jexer/TEditorWindow.properties b/src/jexer/TEditorWindow.properties new file mode 100644 index 0000000..d18b078 --- /dev/null +++ b/src/jexer/TEditorWindow.properties @@ -0,0 +1,10 @@ +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} diff --git a/src/jexer/TExceptionDialog.java b/src/jexer/TExceptionDialog.java new file mode 100644 index 0000000..227aceb --- /dev/null +++ b/src/jexer/TExceptionDialog.java @@ -0,0 +1,207 @@ +/* + * 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 stackTraceStrings = new ArrayList(); + 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; + } + } + } +} diff --git a/src/jexer/TExceptionDialog.properties b/src/jexer/TExceptionDialog.properties new file mode 100644 index 0000000..d07998c --- /dev/null +++ b/src/jexer/TExceptionDialog.properties @@ -0,0 +1,15 @@ +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} diff --git a/src/jexer/TField.java b/src/jexer/TField.java new file mode 100644 index 0000000..7c8b5bc --- /dev/null +++ b/src/jexer/TField.java @@ -0,0 +1,671 @@ +/* + * 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; + } + +} diff --git a/src/jexer/TFileOpenBox.java b/src/jexer/TFileOpenBox.java new file mode 100644 index 0000000..a2cc0cf --- /dev/null +++ b/src/jexer/TFileOpenBox.java @@ -0,0 +1,416 @@ +/* + * 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: + * + *
+ * {@code
+ *     filename = fileOpenBox("/path/to/file.ext",
+ *         TFileOpenBox.Type.OPEN);
+ *     if (filename != null) {
+ *         ... the user selected a file, go open it ...
+ *     }
+ * }
+ * 
+ * + */ +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 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; + } + } + +} diff --git a/src/jexer/TFileOpenBox.properties b/src/jexer/TFileOpenBox.properties new file mode 100644 index 0000000..ef40e86 --- /dev/null +++ b/src/jexer/TFileOpenBox.properties @@ -0,0 +1,7 @@ +openButton=\ &Open\ +openTitle=Open File... +saveButton=\ &Save\ +saveTitle=Save File... +cancelButton=&Cancel +selectButton=S&elect +selectTitle=Select File... diff --git a/src/jexer/TFontChooserWindow.java b/src/jexer/TFontChooserWindow.java new file mode 100644 index 0000000..62eabb6 --- /dev/null +++ b/src/jexer/TFontChooserWindow.java @@ -0,0 +1,628 @@ +/* + * 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 sizes = new ArrayList(); + 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 fonts = new ArrayList(); + 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 ----------------------------------------------------- + // ------------------------------------------------------------------------ + +} diff --git a/src/jexer/TFontChooserWindow.properties b/src/jexer/TFontChooserWindow.properties new file mode 100644 index 0000000..4ab274e --- /dev/null +++ b/src/jexer/TFontChooserWindow.properties @@ -0,0 +1,17 @@ +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\ diff --git a/src/jexer/THScroller.java b/src/jexer/THScroller.java new file mode 100644 index 0000000..a07bcd7 --- /dev/null +++ b/src/jexer/THScroller.java @@ -0,0 +1,407 @@ +/* + * 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; + } + +} diff --git a/src/jexer/TImage.java b/src/jexer/TImage.java new file mode 100644 index 0000000..cd0ce96 --- /dev/null +++ b/src/jexer/TImage.java @@ -0,0 +1,765 @@ +/* + * 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; + } + +} diff --git a/src/jexer/TImageWindow.java b/src/jexer/TImageWindow.java new file mode 100644 index 0000000..15db1da --- /dev/null +++ b/src/jexer/TImageWindow.java @@ -0,0 +1,291 @@ +/* + * 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()); + } + +} diff --git a/src/jexer/TImageWindow.properties b/src/jexer/TImageWindow.properties new file mode 100644 index 0000000..a26fce5 --- /dev/null +++ b/src/jexer/TImageWindow.properties @@ -0,0 +1 @@ +statusBar=Alt-\u2190\u2192-Rotate Left/Right Alt-\u2191\u2193-Bigger/Smaller \u2190\u2192\u2191\u2193-Pan Shift-\u2190\u2192-Scale diff --git a/src/jexer/TInputBox.java b/src/jexer/TInputBox.java new file mode 100644 index 0000000..d60d0b5 --- /dev/null +++ b/src/jexer/TInputBox.java @@ -0,0 +1,138 @@ +/* + * 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: + * + *
+ * {@code
+ *     box = inputBox(title, caption);
+ *     if (box.getText().equals("yes")) {
+ *         ... the user entered "yes", do stuff ...
+ *     }
+ * }
+ * 
+ * + */ +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(); + } + +} diff --git a/src/jexer/TKeypress.java b/src/jexer/TKeypress.java new file mode 100644 index 0000000..c965e7d --- /dev/null +++ b/src/jexer/TKeypress.java @@ -0,0 +1,1028 @@ +/* + * 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 " " + 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; + } + +} diff --git a/src/jexer/TLabel.java b/src/jexer/TLabel.java new file mode 100644 index 0000000..cc341cf --- /dev/null +++ b/src/jexer/TLabel.java @@ -0,0 +1,275 @@ +/* + * 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); + } + } + +} diff --git a/src/jexer/TList.java b/src/jexer/TList.java new file mode 100644 index 0000000..38a994c --- /dev/null +++ b/src/jexer/TList.java @@ -0,0 +1,536 @@ +/* + * 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 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 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 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 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 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(); + 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 getList() { + return new ArrayList(strings); + } + + /** + * Set the new list of strings to display. + * + * @param list new list of strings + */ + public final void setList(final List 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); + } + } + +} diff --git a/src/jexer/TMessageBox.java b/src/jexer/TMessageBox.java new file mode 100644 index 0000000..6f1e8a6 --- /dev/null +++ b/src/jexer/TMessageBox.java @@ -0,0 +1,463 @@ +/* + * 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: + * + *
+ * {@code
+ *     box = messageBox(title, caption,
+ *         TMessageBox.Type.OK | TMessageBox.Type.CANCEL);
+ *
+ *     if (box.getResult() == TMessageBox.OK) {
+ *         ... the user pressed OK, do stuff ...
+ *     }
+ * }
+ * 
+ * + */ +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 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(); + + 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); + } + +} diff --git a/src/jexer/TMessageBox.properties b/src/jexer/TMessageBox.properties new file mode 100644 index 0000000..04e344a --- /dev/null +++ b/src/jexer/TMessageBox.properties @@ -0,0 +1,4 @@ +okButton=\ \ &OK\ \ +cancelButton=&Cancel +yesButton=&Yes +noButton=&No diff --git a/src/jexer/TPanel.java b/src/jexer/TPanel.java new file mode 100644 index 0000000..c38f8e1 --- /dev/null +++ b/src/jexer/TPanel.java @@ -0,0 +1,100 @@ +/* + * 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 ----------------------------------------------------------------- + // ------------------------------------------------------------------------ + + +} diff --git a/src/jexer/TPasswordField.java b/src/jexer/TPasswordField.java new file mode 100644 index 0000000..9c200d7 --- /dev/null +++ b/src/jexer/TPasswordField.java @@ -0,0 +1,132 @@ +/* + * 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(); + } + +} diff --git a/src/jexer/TProgressBar.java b/src/jexer/TProgressBar.java new file mode 100644 index 0000000..38f0337 --- /dev/null +++ b/src/jexer/TProgressBar.java @@ -0,0 +1,294 @@ +/* + * 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; + } + +} diff --git a/src/jexer/TRadioButton.java b/src/jexer/TRadioButton.java new file mode 100644 index 0000000..60a6288 --- /dev/null +++ b/src/jexer/TRadioButton.java @@ -0,0 +1,254 @@ +/* + * 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; + } + +} diff --git a/src/jexer/TRadioGroup.java b/src/jexer/TRadioGroup.java new file mode 100644 index 0000000..a82b074 --- /dev/null +++ b/src/jexer/TRadioGroup.java @@ -0,0 +1,203 @@ +/* + * 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; + } + +} diff --git a/src/jexer/TScrollableWidget.java b/src/jexer/TScrollableWidget.java new file mode 100644 index 0000000..7d15b28 --- /dev/null +++ b/src/jexer/TScrollableWidget.java @@ -0,0 +1,609 @@ +/* + * 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(); + } + } + +} diff --git a/src/jexer/TScrollableWindow.java b/src/jexer/TScrollableWindow.java new file mode 100644 index 0000000..1e260b3 --- /dev/null +++ b/src/jexer/TScrollableWindow.java @@ -0,0 +1,680 @@ +/* + * 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; + } + +} diff --git a/src/jexer/TSpinner.java b/src/jexer/TSpinner.java new file mode 100644 index 0000000..61fac65 --- /dev/null +++ b/src/jexer/TSpinner.java @@ -0,0 +1,191 @@ +/* + * 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); + } + } + +} diff --git a/src/jexer/TSplitPane.java b/src/jexer/TSplitPane.java new file mode 100644 index 0000000..7c85278 --- /dev/null +++ b/src/jexer/TSplitPane.java @@ -0,0 +1,602 @@ +/* + * 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; + } + +} diff --git a/src/jexer/TStatusBar.java b/src/jexer/TStatusBar.java new file mode 100644 index 0000000..fbd79da --- /dev/null +++ b/src/jexer/TStatusBar.java @@ -0,0 +1,329 @@ +/* + * 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 keys = new ArrayList(); + + /** + * 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; + } + +} diff --git a/src/jexer/TTableWidget.java b/src/jexer/TTableWidget.java new file mode 100644 index 0000000..9b4d7c9 --- /dev/null +++ b/src/jexer/TTableWidget.java @@ -0,0 +1,2361 @@ +/* + * 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 columns = new ArrayList(); + + /** + * The underlying data, organized as rows. + */ + private ArrayList rows = new ArrayList(); + + /** + * 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 cells = new ArrayList(); + + /** + * 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 cells = new ArrayList(); + + /** + * 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 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 list = new ArrayList(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(); + } + +} diff --git a/src/jexer/TTableWindow.java b/src/jexer/TTableWindow.java new file mode 100644 index 0000000..44ff7b4 --- /dev/null +++ b/src/jexer/TTableWindow.java @@ -0,0 +1,572 @@ +/* + * 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; + } + +} diff --git a/src/jexer/TTableWindow.properties b/src/jexer/TTableWindow.properties new file mode 100644 index 0000000..c2c8765 --- /dev/null +++ b/src/jexer/TTableWindow.properties @@ -0,0 +1,15 @@ +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} diff --git a/src/jexer/TTerminalWidget.java b/src/jexer/TTerminalWidget.java new file mode 100644 index 0000000..a269609 --- /dev/null +++ b/src/jexer/TTerminalWidget.java @@ -0,0 +1,1156 @@ +/* + * 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 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 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; + } + +} diff --git a/src/jexer/TTerminalWidget.properties b/src/jexer/TTerminalWidget.properties new file mode 100644 index 0000000..ecfcf21 --- /dev/null +++ b/src/jexer/TTerminalWidget.properties @@ -0,0 +1,6 @@ +windowTitle=Terminal +errorLaunchingShellTitle=Error +errorLaunchingShellText=Error launching shell: {0} +statusBarRunning=Terminal session executing... +windowTitleCompleted={0} [Completed - {1}] +statusBarCompleted=Terminal session completed, exit code {0}. diff --git a/src/jexer/TTerminalWindow.java b/src/jexer/TTerminalWindow.java new file mode 100644 index 0000000..e96c50c --- /dev/null +++ b/src/jexer/TTerminalWindow.java @@ -0,0 +1,455 @@ +/* + * 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)); + } + +} diff --git a/src/jexer/TTerminalWindow.properties b/src/jexer/TTerminalWindow.properties new file mode 100644 index 0000000..ed22f49 --- /dev/null +++ b/src/jexer/TTerminalWindow.properties @@ -0,0 +1,2 @@ +windowTitle=Terminal +statusBarRunning=Terminal session executing... diff --git a/src/jexer/TText.java b/src/jexer/TText.java new file mode 100644 index 0000000..22bc4b8 --- /dev/null +++ b/src/jexer/TText.java @@ -0,0 +1,445 @@ +/* + * 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 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(); + + 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(); + } + +} diff --git a/src/jexer/TTimer.java b/src/jexer/TTimer.java new file mode 100644 index 0000000..8007153 --- /dev/null +++ b/src/jexer/TTimer.java @@ -0,0 +1,120 @@ +/* + * 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); + } + } + +} diff --git a/src/jexer/TVScroller.java b/src/jexer/TVScroller.java new file mode 100644 index 0000000..444e058 --- /dev/null +++ b/src/jexer/TVScroller.java @@ -0,0 +1,402 @@ +/* + * 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; + } + +} diff --git a/src/jexer/TWidget.java b/src/jexer/TWidget.java new file mode 100644 index 0000000..eb06175 --- /dev/null +++ b/src/jexer/TWidget.java @@ -0,0 +1,2767 @@ +/* + * 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 { + + // ------------------------------------------------------------------------ + // 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 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(); + } + + /** + * 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(); + + 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 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: + *
    + *
  • tabOrder for TWidgets
  • + *
  • z for TWindows
  • + *
  • text for TTreeItems
  • + *
+ * + * @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}. + *

+ * 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 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 filters = new ArrayList(); + 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 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 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 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 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 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 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); + } + +} diff --git a/src/jexer/TWindow.java b/src/jexer/TWindow.java new file mode 100644 index 0000000..58195c9 --- /dev/null +++ b/src/jexer/TWindow.java @@ -0,0 +1,1455 @@ +/* + * 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 keyboardShortcuts = new HashSet(); + + /** + * 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()); + } + +} diff --git a/src/jexer/backend/Backend.java b/src/jexer/backend/Backend.java new file mode 100644 index 0000000..eaed7e6 --- /dev/null +++ b/src/jexer/backend/Backend.java @@ -0,0 +1,104 @@ +/* + * 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 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(); + +} diff --git a/src/jexer/backend/ECMA48Backend.java b/src/jexer/backend/ECMA48Backend.java new file mode 100644 index 0000000..0614e17 --- /dev/null +++ b/src/jexer/backend/ECMA48Backend.java @@ -0,0 +1,168 @@ +/* + * 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); + } + +} diff --git a/src/jexer/backend/ECMA48Terminal.java b/src/jexer/backend/ECMA48Terminal.java new file mode 100644 index 0000000..e2997d2 --- /dev/null +++ b/src/jexer/backend/ECMA48Terminal.java @@ -0,0 +1,4384 @@ +/* + * 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 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 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 rgbColors = new ArrayList(); + + /** + * 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>> 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> 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 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("\n"); + // Hue is evenly spaced around the wheel. + hslColors = new ArrayList>>(); + + final boolean DEBUG = false; + ArrayList rawRgbList = new ArrayList(); + + for (int hue = 0; hue < (360 - (360 % hueStep)); + hue += (360/hueStep)) { + + ArrayList> satList = null; + satList = new ArrayList>(); + hslColors.add(satList); + + // Saturation is linearly spaced between pastel and pure. + for (int sat = satStep; sat <= 100; sat += satStep) { + + ArrayList lumList = new ArrayList(); + satList.add(lumList); + + // Luminance brackets the pure color, but leaning toward + // lighter. + for (int lum = lumBegin; lum < 100; lum += lumStep) { + /* + System.err.printf("=\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\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 rgbColorIndices = null; + rgbColorIndices = new HashMap(); + 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("\n"); + for (Integer rgb: rgbColors) { + System.err.printf("=\n"); + } + System.err.printf("\n\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 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(); + } + + /** + * Make a unique key for a list of cells. + * + * @param cells the cells + * @return the key + */ + private String makeKey(final ArrayList 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 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 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(); + 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(); + 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 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 events = new ArrayList(); + + 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. + * + *

Actually executes '/bin/sh -c stty sane cooked < /dev/tty' + */ + private void sttyCooked() { + doStty(false); + } + + /** + * Call 'stty' to set raw mode. + * + *

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 cellsToDraw = new ArrayList(); + 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(); + 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 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 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 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 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 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\\"; + } + +} diff --git a/src/jexer/backend/GenericBackend.java b/src/jexer/backend/GenericBackend.java new file mode 100644 index 0000000..ede3c0b --- /dev/null +++ b/src/jexer/backend/GenericBackend.java @@ -0,0 +1,171 @@ +/* + * 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 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(); + } + +} diff --git a/src/jexer/backend/GlyphMaker.java b/src/jexer/backend/GlyphMaker.java new file mode 100644 index 0000000..0da2918 --- /dev/null +++ b/src/jexer/backend/GlyphMaker.java @@ -0,0 +1,472 @@ +/* + * 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 glyphCacheBlink; + + /** + * A cache of previously-rendered glyphs for non-blinking, or + * blinking-and-visible, text. + */ + private HashMap 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(); + glyphCache = new HashMap(); + + 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 makers = new HashMap(); + + /** + * 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); + } + +} diff --git a/src/jexer/backend/LogicalScreen.java b/src/jexer/backend/LogicalScreen.java new file mode 100644 index 0000000..4e4aecc --- /dev/null +++ b/src/jexer/backend/LogicalScreen.java @@ -0,0 +1,1045 @@ +/* + * 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); + } + +} diff --git a/src/jexer/backend/MultiBackend.java b/src/jexer/backend/MultiBackend.java new file mode 100644 index 0000000..d01b944 --- /dev/null +++ b/src/jexer/backend/MultiBackend.java @@ -0,0 +1,254 @@ +/* + * 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 backends = new ArrayList(); + + /** + * 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 queue) { + List 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(); + } + 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); + } + } + +} diff --git a/src/jexer/backend/MultiScreen.java b/src/jexer/backend/MultiScreen.java new file mode 100644 index 0000000..9d66b69 --- /dev/null +++ b/src/jexer/backend/MultiScreen.java @@ -0,0 +1,673 @@ +/* + * 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 screens = new ArrayList(); + + // ------------------------------------------------------------------------ + // 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; + } + +} diff --git a/src/jexer/backend/Screen.java b/src/jexer/backend/Screen.java new file mode 100644 index 0000000..2a71073 --- /dev/null +++ b/src/jexer/backend/Screen.java @@ -0,0 +1,412 @@ +/* + * 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(); + +} diff --git a/src/jexer/backend/SessionInfo.java b/src/jexer/backend/SessionInfo.java new file mode 100644 index 0000000..8a29ce0 --- /dev/null +++ b/src/jexer/backend/SessionInfo.java @@ -0,0 +1,83 @@ +/* + * 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(); +} diff --git a/src/jexer/backend/SwingBackend.java b/src/jexer/backend/SwingBackend.java new file mode 100644 index 0000000..8a342b6 --- /dev/null +++ b/src/jexer/backend/SwingBackend.java @@ -0,0 +1,167 @@ +/* + * 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(); + } + +} diff --git a/src/jexer/backend/SwingComponent.java b/src/jexer/backend/SwingComponent.java new file mode 100644 index 0000000..3d1074c --- /dev/null +++ b/src/jexer/backend/SwingComponent.java @@ -0,0 +1,601 @@ +/* + * 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(); + } + } + +} diff --git a/src/jexer/backend/SwingSessionInfo.java b/src/jexer/backend/SwingSessionInfo.java new file mode 100644 index 0000000..2f74d70 --- /dev/null +++ b/src/jexer/backend/SwingSessionInfo.java @@ -0,0 +1,226 @@ +/* + * 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; + } + +} diff --git a/src/jexer/backend/SwingTerminal.java b/src/jexer/backend/SwingTerminal.java new file mode 100644 index 0000000..f0ba355 --- /dev/null +++ b/src/jexer/backend/SwingTerminal.java @@ -0,0 +1,2331 @@ +/* + * 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 glyphCacheBlink; + + /** + * A cache of previously-rendered glyphs for non-blinking, or + * blinking-and-visible, text. + */ + private Map 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 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(); + + // 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(); + + // 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 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(); + glyphCache = new HashMap(); + 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(); + glyphCache = new HashMap(); + 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(); + glyphCache = new HashMap(); + 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(); + glyphCache = new HashMap(); + 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(); + glyphCache = new HashMap(); + 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(); + glyphCache = new HashMap(); + 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(); + } + } + } + +} diff --git a/src/jexer/backend/TSessionInfo.java b/src/jexer/backend/TSessionInfo.java new file mode 100644 index 0000000..ccddce4 --- /dev/null +++ b/src/jexer/backend/TSessionInfo.java @@ -0,0 +1,148 @@ +/* + * 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 + } + +} diff --git a/src/jexer/backend/TTYSessionInfo.java b/src/jexer/backend/TTYSessionInfo.java new file mode 100644 index 0000000..d7f5bc8 --- /dev/null +++ b/src/jexer/backend/TTYSessionInfo.java @@ -0,0 +1,228 @@ +/* + * 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(); + } + } + +} diff --git a/src/jexer/backend/TWindowBackend.java b/src/jexer/backend/TWindowBackend.java new file mode 100644 index 0000000..f644b76 --- /dev/null +++ b/src/jexer/backend/TWindowBackend.java @@ -0,0 +1,543 @@ +/* + * 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 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(); + 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(); + 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(); + 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(); + 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 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; + } + +} diff --git a/src/jexer/backend/TerminalReader.java b/src/jexer/backend/TerminalReader.java new file mode 100644 index 0000000..32033e0 --- /dev/null +++ b/src/jexer/backend/TerminalReader.java @@ -0,0 +1,74 @@ +/* + * 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 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(); + +} diff --git a/src/jexer/backend/package-info.java b/src/jexer/backend/package-info.java new file mode 100644 index 0000000..46d8ba1 --- /dev/null +++ b/src/jexer/backend/package-info.java @@ -0,0 +1,33 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2019 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ + +/** + * The interface between TApplication and user-facing I/O. + */ +package jexer.backend; diff --git a/src/jexer/bits/Cell.java b/src/jexer/bits/Cell.java new file mode 100644 index 0000000..a8efa2b --- /dev/null +++ b/src/jexer/bits/Cell.java @@ -0,0 +1,485 @@ +/* + * 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); + } +} diff --git a/src/jexer/bits/CellAttributes.java b/src/jexer/bits/CellAttributes.java new file mode 100644 index 0000000..99366fd --- /dev/null +++ b/src/jexer/bits/CellAttributes.java @@ -0,0 +1,395 @@ +/* + * 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); + } + +} diff --git a/src/jexer/bits/Color.java b/src/jexer/bits/Color.java new file mode 100644 index 0000000..4defed5 --- /dev/null +++ b/src/jexer/bits/Color.java @@ -0,0 +1,272 @@ +/* + * 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); + } + } + +} diff --git a/src/jexer/bits/ColorTheme.java b/src/jexer/bits/ColorTheme.java new file mode 100644 index 0000000..ffba4d4 --- /dev/null +++ b/src/jexer/bits/ColorTheme.java @@ -0,0 +1,688 @@ +/* + * 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 colors; + + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Public constructor sets the theme to the default. + */ + public ColorTheme() { + colors = new TreeMap(); + 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 getColorNames() { + Set keys = colors.keySet(); + List names = new ArrayList(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: + * [ bold ] [ blink ] { foreground on background } + * + * @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(); + } + +} diff --git a/src/jexer/bits/GraphicsChars.java b/src/jexer/bits/GraphicsChars.java new file mode 100644 index 0000000..58be231 --- /dev/null +++ b/src/jexer/bits/GraphicsChars.java @@ -0,0 +1,161 @@ +/* + * 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() { + } + +} diff --git a/src/jexer/bits/MnemonicString.java b/src/jexer/bits/MnemonicString.java new file mode 100644 index 0000000..58575b5 --- /dev/null +++ b/src/jexer/bits/MnemonicString.java @@ -0,0 +1,154 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2019 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ +package jexer.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; + } + +} diff --git a/src/jexer/bits/StringUtils.java b/src/jexer/bits/StringUtils.java new file mode 100644 index 0000000..fffce20 --- /dev/null +++ b/src/jexer/bits/StringUtils.java @@ -0,0 +1,498 @@ +/* + * 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 left(final String str, final int n) { + List result = new ArrayList(); + + /* + * 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 right(final String str, final int n) { + List result = new ArrayList(); + + /* + * Same as left(), but preceed each line with spaces to make it n + * chars long. + */ + List 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 center(final String str, final int n) { + List result = new ArrayList(); + + /* + * Same as left(), but preceed/succeed each line with spaces to make + * it n chars long. + */ + List 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 full(final String str, final int n) { + List result = new ArrayList(); + + /* + * 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 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 fromCsv(final String line) { + List result = new ArrayList(); + + 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 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)); + } + +} diff --git a/src/jexer/bits/package-info.java b/src/jexer/bits/package-info.java new file mode 100644 index 0000000..cffe10e --- /dev/null +++ b/src/jexer/bits/package-info.java @@ -0,0 +1,34 @@ +/* + * 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; diff --git a/src/jexer/demos/Demo1.java b/src/jexer/demos/Demo1.java new file mode 100644 index 0000000..97088d2 --- /dev/null +++ b/src/jexer/demos/Demo1.java @@ -0,0 +1,69 @@ +/* + * 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(); + } + } + +} diff --git a/src/jexer/demos/Demo2.java b/src/jexer/demos/Demo2.java new file mode 100644 index 0000000..2db03ce --- /dev/null +++ b/src/jexer/demos/Demo2.java @@ -0,0 +1,99 @@ +/* + * 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 + } + } + } + } + +} diff --git a/src/jexer/demos/Demo2.properties b/src/jexer/demos/Demo2.properties new file mode 100644 index 0000000..fa2b98f --- /dev/null +++ b/src/jexer/demos/Demo2.properties @@ -0,0 +1,5 @@ +usageString=USAGE: java -cp jexer.jar jexer.demos.Demo2 port +newConnection=New connection: {0} +username=\ \ \ username: {0} +language=\ \ \ language: {0} +terminal=\ \ \ terminal: {0} diff --git a/src/jexer/demos/Demo3.java b/src/jexer/demos/Demo3.java new file mode 100644 index 0000000..f370f8f --- /dev/null +++ b/src/jexer/demos/Demo3.java @@ -0,0 +1,57 @@ +/* + * 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(); + } + } + +} diff --git a/src/jexer/demos/Demo4.java b/src/jexer/demos/Demo4.java new file mode 100644 index 0000000..edbc2c0 --- /dev/null +++ b/src/jexer/demos/Demo4.java @@ -0,0 +1,69 @@ +/* + * 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(); + } + } + +} diff --git a/src/jexer/demos/Demo5.java b/src/jexer/demos/Demo5.java new file mode 100644 index 0000000..e63abc1 --- /dev/null +++ b/src/jexer/demos/Demo5.java @@ -0,0 +1,224 @@ +/* + * 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(); + } + } + +} diff --git a/src/jexer/demos/Demo5.properties b/src/jexer/demos/Demo5.properties new file mode 100644 index 0000000..56b419d --- /dev/null +++ b/src/jexer/demos/Demo5.properties @@ -0,0 +1 @@ +frameTitle=Two Jexer Apps In One Swing UI diff --git a/src/jexer/demos/Demo6.java b/src/jexer/demos/Demo6.java new file mode 100644 index 0000000..db0b5c9 --- /dev/null +++ b/src/jexer/demos/Demo6.java @@ -0,0 +1,148 @@ +/* + * 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(); + } + } + +} diff --git a/src/jexer/demos/Demo6.properties b/src/jexer/demos/Demo6.properties new file mode 100644 index 0000000..450829a --- /dev/null +++ b/src/jexer/demos/Demo6.properties @@ -0,0 +1 @@ +monitorWindow=Monitor Window diff --git a/src/jexer/demos/Demo7.java b/src/jexer/demos/Demo7.java new file mode 100644 index 0000000..5f92347 --- /dev/null +++ b/src/jexer/demos/Demo7.java @@ -0,0 +1,102 @@ +/* + * 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(); + } + +} diff --git a/src/jexer/demos/Demo7.properties b/src/jexer/demos/Demo7.properties new file mode 100644 index 0000000..e6fd7ee --- /dev/null +++ b/src/jexer/demos/Demo7.properties @@ -0,0 +1 @@ +windowTitle=BoxLayoutManager Demo diff --git a/src/jexer/demos/DemoApplication.java b/src/jexer/demos/DemoApplication.java new file mode 100644 index 0000000..3e4cbe9 --- /dev/null +++ b/src/jexer/demos/DemoApplication.java @@ -0,0 +1,247 @@ +/* + * 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(); + } + +} diff --git a/src/jexer/demos/DemoApplication.properties b/src/jexer/demos/DemoApplication.properties new file mode 100644 index 0000000..95d8603 --- /dev/null +++ b/src/jexer/demos/DemoApplication.properties @@ -0,0 +1,15 @@ +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 diff --git a/src/jexer/demos/DemoCheckBoxWindow.java b/src/jexer/demos/DemoCheckBoxWindow.java new file mode 100644 index 0000000..fda7bd7 --- /dev/null +++ b/src/jexer/demos/DemoCheckBoxWindow.java @@ -0,0 +1,154 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2019 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ +package jexer.demos; + +import java.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 comboValues = new ArrayList(); + 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")); + } + +} diff --git a/src/jexer/demos/DemoCheckBoxWindow.properties b/src/jexer/demos/DemoCheckBoxWindow.properties new file mode 100644 index 0000000..61210ce --- /dev/null +++ b/src/jexer/demos/DemoCheckBoxWindow.properties @@ -0,0 +1,30 @@ +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 diff --git a/src/jexer/demos/DemoEditorWindow.java b/src/jexer/demos/DemoEditorWindow.java new file mode 100644 index 0000000..87798fb --- /dev/null +++ b/src/jexer/demos/DemoEditorWindow.java @@ -0,0 +1,148 @@ +/* + * 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); + } + } + +} diff --git a/src/jexer/demos/DemoEditorWindow.properties b/src/jexer/demos/DemoEditorWindow.properties new file mode 100644 index 0000000..3fa3212 --- /dev/null +++ b/src/jexer/demos/DemoEditorWindow.properties @@ -0,0 +1,6 @@ +windowTitle=Editor + +statusBar=Editable text demo window +statusBarHelp=Help +statusBarShell=Shell +statusBarExit=Exit diff --git a/src/jexer/demos/DemoMainWindow.java b/src/jexer/demos/DemoMainWindow.java new file mode 100644 index 0000000..8f77448 --- /dev/null +++ b/src/jexer/demos/DemoMainWindow.java @@ -0,0 +1,370 @@ +/* + * 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); + } + +} diff --git a/src/jexer/demos/DemoMainWindow.properties b/src/jexer/demos/DemoMainWindow.properties new file mode 100644 index 0000000..dba1cb0 --- /dev/null +++ b/src/jexer/demos/DemoMainWindow.properties @@ -0,0 +1,39 @@ +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 diff --git a/src/jexer/demos/DemoMsgBoxWindow.java b/src/jexer/demos/DemoMsgBoxWindow.java new file mode 100644 index 0000000..0485f51 --- /dev/null +++ b/src/jexer/demos/DemoMsgBoxWindow.java @@ -0,0 +1,190 @@ +/* + * 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")); + } +} diff --git a/src/jexer/demos/DemoMsgBoxWindow.properties b/src/jexer/demos/DemoMsgBoxWindow.properties new file mode 100644 index 0000000..47a858a --- /dev/null +++ b/src/jexer/demos/DemoMsgBoxWindow.properties @@ -0,0 +1,45 @@ +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 diff --git a/src/jexer/demos/DemoTableWindow.java b/src/jexer/demos/DemoTableWindow.java new file mode 100644 index 0000000..85da32a --- /dev/null +++ b/src/jexer/demos/DemoTableWindow.java @@ -0,0 +1,118 @@ +/* + * 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); + } + } + +} diff --git a/src/jexer/demos/DemoTableWindow.properties b/src/jexer/demos/DemoTableWindow.properties new file mode 100644 index 0000000..ecc9ec5 --- /dev/null +++ b/src/jexer/demos/DemoTableWindow.properties @@ -0,0 +1,6 @@ +windowTitle=Table + +statusBar=Table datagrid demo window +statusBarHelp=Help +statusBarShell=Shell +statusBarExit=Exit diff --git a/src/jexer/demos/DemoTextFieldWindow.java b/src/jexer/demos/DemoTextFieldWindow.java new file mode 100644 index 0000000..2c6116a --- /dev/null +++ b/src/jexer/demos/DemoTextFieldWindow.java @@ -0,0 +1,182 @@ +/* + * 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")); + } + +} diff --git a/src/jexer/demos/DemoTextFieldWindow.properties b/src/jexer/demos/DemoTextFieldWindow.properties new file mode 100644 index 0000000..5b42990 --- /dev/null +++ b/src/jexer/demos/DemoTextFieldWindow.properties @@ -0,0 +1,17 @@ +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 diff --git a/src/jexer/demos/DemoTextWindow.java b/src/jexer/demos/DemoTextWindow.java new file mode 100644 index 0000000..7490886 --- /dev/null +++ b/src/jexer/demos/DemoTextWindow.java @@ -0,0 +1,181 @@ +/* + * 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); + } + +} diff --git a/src/jexer/demos/DemoTextWindow.properties b/src/jexer/demos/DemoTextWindow.properties new file mode 100644 index 0000000..873a56f --- /dev/null +++ b/src/jexer/demos/DemoTextWindow.properties @@ -0,0 +1,12 @@ +windowTitle=Text Area + +statusBar=Reflowable text window +statusBarHelp=Help +statusBarShell=Shell +statusBarOpen=Open +statusBarExit=Exit + +left=&Left +center=&Center +right=&Right +full=&Full diff --git a/src/jexer/demos/DemoTreeViewWindow.java b/src/jexer/demos/DemoTreeViewWindow.java new file mode 100644 index 0000000..4798951 --- /dev/null +++ b/src/jexer/demos/DemoTreeViewWindow.java @@ -0,0 +1,116 @@ +/* + * 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); + } + } + +} diff --git a/src/jexer/demos/DemoTreeViewWindow.properties b/src/jexer/demos/DemoTreeViewWindow.properties new file mode 100644 index 0000000..d63b24e --- /dev/null +++ b/src/jexer/demos/DemoTreeViewWindow.properties @@ -0,0 +1,7 @@ +windowTitle=Tree View + +statusBar=Treeview demonstration +statusBarHelp=Help +statusBarShell=Shell +statusBarOpen=Open +statusBarExit=Exit diff --git a/src/jexer/demos/DesktopDemo.java b/src/jexer/demos/DesktopDemo.java new file mode 100644 index 0000000..520f5b0 --- /dev/null +++ b/src/jexer/demos/DesktopDemo.java @@ -0,0 +1,75 @@ +/* + * 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(); + } + } + +} diff --git a/src/jexer/demos/DesktopDemoApplication.java b/src/jexer/demos/DesktopDemoApplication.java new file mode 100644 index 0000000..73d0c5f --- /dev/null +++ b/src/jexer/demos/DesktopDemoApplication.java @@ -0,0 +1,268 @@ +/* + * 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); + } + } + ); + } + +} diff --git a/src/jexer/demos/DesktopDemoApplication.properties b/src/jexer/demos/DesktopDemoApplication.properties new file mode 100644 index 0000000..85f7435 --- /dev/null +++ b/src/jexer/demos/DesktopDemoApplication.properties @@ -0,0 +1,19 @@ +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 diff --git a/src/jexer/demos/package-info.java b/src/jexer/demos/package-info.java new file mode 100644 index 0000000..1305cdd --- /dev/null +++ b/src/jexer/demos/package-info.java @@ -0,0 +1,33 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2019 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ + +/** + * Demonstration programs. + */ +package jexer.demos; diff --git a/src/jexer/event/TCommandEvent.java b/src/jexer/event/TCommandEvent.java new file mode 100644 index 0000000..60f6385 --- /dev/null +++ b/src/jexer/event/TCommandEvent.java @@ -0,0 +1,128 @@ +/* + * 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; + } + +} diff --git a/src/jexer/event/TInputEvent.java b/src/jexer/event/TInputEvent.java new file mode 100644 index 0000000..220512f --- /dev/null +++ b/src/jexer/event/TInputEvent.java @@ -0,0 +1,72 @@ +/* + * 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; + } + +} diff --git a/src/jexer/event/TKeypressEvent.java b/src/jexer/event/TKeypressEvent.java new file mode 100644 index 0000000..79b28f2 --- /dev/null +++ b/src/jexer/event/TKeypressEvent.java @@ -0,0 +1,167 @@ +/* + * 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; + } + +} diff --git a/src/jexer/event/TMenuEvent.java b/src/jexer/event/TMenuEvent.java new file mode 100644 index 0000000..e2ff7c7 --- /dev/null +++ b/src/jexer/event/TMenuEvent.java @@ -0,0 +1,87 @@ +/* + * 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; + } + +} diff --git a/src/jexer/event/TMouseEvent.java b/src/jexer/event/TMouseEvent.java new file mode 100644 index 0000000..496d8bc --- /dev/null +++ b/src/jexer/event/TMouseEvent.java @@ -0,0 +1,321 @@ +/* + * 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); + } + +} diff --git a/src/jexer/event/TResizeEvent.java b/src/jexer/event/TResizeEvent.java new file mode 100644 index 0000000..ff95710 --- /dev/null +++ b/src/jexer/event/TResizeEvent.java @@ -0,0 +1,134 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2019 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ +package jexer.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); + } + +} diff --git a/src/jexer/event/package-info.java b/src/jexer/event/package-info.java new file mode 100644 index 0000000..e4541a3 --- /dev/null +++ b/src/jexer/event/package-info.java @@ -0,0 +1,34 @@ +/* + * 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; diff --git a/src/jexer/io/ReadTimeoutException.java b/src/jexer/io/ReadTimeoutException.java new file mode 100644 index 0000000..8c6371e --- /dev/null +++ b/src/jexer/io/ReadTimeoutException.java @@ -0,0 +1,52 @@ +/* + * 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); + } +} diff --git a/src/jexer/io/TimeoutInputStream.java b/src/jexer/io/TimeoutInputStream.java new file mode 100644 index 0000000..3d8cdb0 --- /dev/null +++ b/src/jexer/io/TimeoutInputStream.java @@ -0,0 +1,393 @@ +/* + * 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; + } + +} diff --git a/src/jexer/io/package-info.java b/src/jexer/io/package-info.java new file mode 100644 index 0000000..37ad2bb --- /dev/null +++ b/src/jexer/io/package-info.java @@ -0,0 +1,33 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2019 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ + +/** + * java.io subclasses. + */ +package jexer.io; diff --git a/src/jexer/layout/BoxLayoutManager.java b/src/jexer/layout/BoxLayoutManager.java new file mode 100644 index 0000000..057127f --- /dev/null +++ b/src/jexer/layout/BoxLayoutManager.java @@ -0,0 +1,170 @@ +/* + * 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 children = new ArrayList(); + + // ------------------------------------------------------------------------ + // 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); + } + } + +} diff --git a/src/jexer/layout/LayoutManager.java b/src/jexer/layout/LayoutManager.java new file mode 100644 index 0000000..5dbd1e8 --- /dev/null +++ b/src/jexer/layout/LayoutManager.java @@ -0,0 +1,69 @@ +/* + * 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); + +} diff --git a/src/jexer/layout/StretchLayoutManager.java b/src/jexer/layout/StretchLayoutManager.java new file mode 100644 index 0000000..ee2bf5a --- /dev/null +++ b/src/jexer/layout/StretchLayoutManager.java @@ -0,0 +1,165 @@ +/* + * 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 children = new HashMap(); + + // ------------------------------------------------------------------------ + // 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)); + } + } + +} diff --git a/src/jexer/layout/package-info.java b/src/jexer/layout/package-info.java new file mode 100644 index 0000000..69887dd --- /dev/null +++ b/src/jexer/layout/package-info.java @@ -0,0 +1,33 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2019 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ + +/** + * Available layout managers. + */ +package jexer.layout; diff --git a/src/jexer/menu/TMenu.java b/src/jexer/menu/TMenu.java new file mode 100644 index 0000000..6d746df --- /dev/null +++ b/src/jexer/menu/TMenu.java @@ -0,0 +1,822 @@ +/* + * 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; + } + +} diff --git a/src/jexer/menu/TMenu.properties b/src/jexer/menu/TMenu.properties new file mode 100644 index 0000000..4a0f8e6 --- /dev/null +++ b/src/jexer/menu/TMenu.properties @@ -0,0 +1,62 @@ +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... + diff --git a/src/jexer/menu/TMenuItem.java b/src/jexer/menu/TMenuItem.java new file mode 100644 index 0000000..d9dfc2a --- /dev/null +++ b/src/jexer/menu/TMenuItem.java @@ -0,0 +1,339 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2019 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ +package jexer.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; + } + } + +} diff --git a/src/jexer/menu/TMenuSeparator.java b/src/jexer/menu/TMenuSeparator.java new file mode 100644 index 0000000..0528e5d --- /dev/null +++ b/src/jexer/menu/TMenuSeparator.java @@ -0,0 +1,73 @@ +/* + * 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); + } + +} diff --git a/src/jexer/menu/TSubMenu.java b/src/jexer/menu/TSubMenu.java new file mode 100644 index 0000000..e285c5a --- /dev/null +++ b/src/jexer/menu/TSubMenu.java @@ -0,0 +1,267 @@ +/* + * 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); + } + +} diff --git a/src/jexer/menu/package-info.java b/src/jexer/menu/package-info.java new file mode 100644 index 0000000..2c10393 --- /dev/null +++ b/src/jexer/menu/package-info.java @@ -0,0 +1,33 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2019 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ + +/** + * Menu bar support classes. + */ +package jexer.menu; diff --git a/src/jexer/net/TelnetInputStream.java b/src/jexer/net/TelnetInputStream.java new file mode 100644 index 0000000..be3ab50 --- /dev/null +++ b/src/jexer/net/TelnetInputStream.java @@ -0,0 +1,1399 @@ +/* + * 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 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(); + } + + // ------------------------------------------------------------------------ + // 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. + * + *

The options we use are: + * + *

+ *

+     *     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
+     * 
+ * + * @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 newEnv = new TreeMap(); + + 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; + } + +} diff --git a/src/jexer/net/TelnetOutputStream.java b/src/jexer/net/TelnetOutputStream.java new file mode 100644 index 0000000..6e7536a --- /dev/null +++ b/src/jexer/net/TelnetOutputStream.java @@ -0,0 +1,260 @@ +/* + * 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 -> 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 ). + 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 -> 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 -> 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 -> 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); + } + } + +} diff --git a/src/jexer/net/TelnetServerSocket.java b/src/jexer/net/TelnetServerSocket.java new file mode 100644 index 0000000..3c5b307 --- /dev/null +++ b/src/jexer/net/TelnetServerSocket.java @@ -0,0 +1,129 @@ +/* + * 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; + } + +} diff --git a/src/jexer/net/TelnetSocket.java b/src/jexer/net/TelnetSocket.java new file mode 100644 index 0000000..ac8a278 --- /dev/null +++ b/src/jexer/net/TelnetSocket.java @@ -0,0 +1,203 @@ +/* + * 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); + } + +} diff --git a/src/jexer/net/package-info.java b/src/jexer/net/package-info.java new file mode 100644 index 0000000..5d738fb --- /dev/null +++ b/src/jexer/net/package-info.java @@ -0,0 +1,33 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2019 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ + +/** + * A Telnet-aware ServerSocket that establishes an 8-bit clean data channel. + */ +package jexer.net; diff --git a/src/jexer/package-info.java b/src/jexer/package-info.java new file mode 100644 index 0000000..300f973 --- /dev/null +++ b/src/jexer/package-info.java @@ -0,0 +1,63 @@ +/* + * 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 + * + *

+ * This library is a text-based windowing system loosely reminiscent of + * Borland's Turbo + * Vision 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: + * + *

+ * {@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();
+ *     }
+ * }
+ * }
+ * 
+ */ +package jexer; diff --git a/src/jexer/resources/jexer_logo_128.png b/src/jexer/resources/jexer_logo_128.png new file mode 100644 index 0000000..5c3a813 Binary files /dev/null and b/src/jexer/resources/jexer_logo_128.png differ diff --git a/src/jexer/resources/terminus-ttf-4.39/COPYING b/src/jexer/resources/terminus-ttf-4.39/COPYING new file mode 100644 index 0000000..c964194 --- /dev/null +++ b/src/jexer/resources/terminus-ttf-4.39/COPYING @@ -0,0 +1,97 @@ +Copyright (c) 2010 Dimitar Toshkov Zhekov, +with Reserved Font Name "Terminus Font". + +Copyright (c) 2011 Tilman Blumenbach, +with Reserved Font Name "Terminus (TTF)". + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/src/jexer/resources/terminus-ttf-4.39/TerminusTTF-4.39.ttf b/src/jexer/resources/terminus-ttf-4.39/TerminusTTF-4.39.ttf new file mode 100644 index 0000000..f4bb6b2 Binary files /dev/null and b/src/jexer/resources/terminus-ttf-4.39/TerminusTTF-4.39.ttf differ diff --git a/src/jexer/resources/terminus-ttf-4.39/TerminusTTF-Bold-4.39.ttf b/src/jexer/resources/terminus-ttf-4.39/TerminusTTF-Bold-4.39.ttf new file mode 100644 index 0000000..06700de Binary files /dev/null and b/src/jexer/resources/terminus-ttf-4.39/TerminusTTF-Bold-4.39.ttf differ diff --git a/src/jexer/resources/terminus-ttf-4.39/TerminusTTF-Italic-4.39.ttf b/src/jexer/resources/terminus-ttf-4.39/TerminusTTF-Italic-4.39.ttf new file mode 100644 index 0000000..e80d0c0 Binary files /dev/null and b/src/jexer/resources/terminus-ttf-4.39/TerminusTTF-Italic-4.39.ttf differ diff --git a/src/jexer/teditor/Document.java b/src/jexer/teditor/Document.java new file mode 100644 index 0000000..2abfef6 --- /dev/null +++ b/src/jexer/teditor/Document.java @@ -0,0 +1,640 @@ +/* + * 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 lines = new ArrayList(); + + /** + * 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 getLines() { + return new ArrayList(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(); + } + +} diff --git a/src/jexer/teditor/Highlighter.java b/src/jexer/teditor/Highlighter.java new file mode 100644 index 0000000..a484194 --- /dev/null +++ b/src/jexer/teditor/Highlighter.java @@ -0,0 +1,146 @@ +/* + * 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 colors; + + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Public constructor sets the theme to the default. + */ + public Highlighter() { + colors = new TreeMap(); + } + + // ------------------------------------------------------------------------ + // 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); + } + + } + +} diff --git a/src/jexer/teditor/Line.java b/src/jexer/teditor/Line.java new file mode 100644 index 0000000..7cd5feb --- /dev/null +++ b/src/jexer/teditor/Line.java @@ -0,0 +1,365 @@ +/* + * 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 words = new ArrayList(); + + /** + * 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 getWords() { + return new ArrayList(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()); + } + +} diff --git a/src/jexer/teditor/Word.java b/src/jexer/teditor/Word.java new file mode 100644 index 0000000..eada29c --- /dev/null +++ b/src/jexer/teditor/Word.java @@ -0,0 +1,226 @@ +/* + * 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; + } + +} diff --git a/src/jexer/teditor/package-info.java b/src/jexer/teditor/package-info.java new file mode 100644 index 0000000..8bf5199 --- /dev/null +++ b/src/jexer/teditor/package-info.java @@ -0,0 +1,33 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2019 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ + +/** + * A basic text editor backend supporting word highlighting. + */ +package jexer.teditor; diff --git a/src/jexer/tterminal/DECCharacterSets.java b/src/jexer/tterminal/DECCharacterSets.java new file mode 100644 index 0000000..bca81bb --- /dev/null +++ b/src/jexer/tterminal/DECCharacterSets.java @@ -0,0 +1,381 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2019 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ +package jexer.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() { + } + +} diff --git a/src/jexer/tterminal/DisplayLine.java b/src/jexer/tterminal/DisplayLine.java new file mode 100644 index 0000000..06a05a3 --- /dev/null +++ b/src/jexer/tterminal/DisplayLine.java @@ -0,0 +1,251 @@ +/* + * 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: + * + *

+     *   0 = single height
+     *   1 = top half double height
+     *   2 = bottom half double height
+     * 
+ */ + 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); + } + +} diff --git a/src/jexer/tterminal/DisplayListener.java b/src/jexer/tterminal/DisplayListener.java new file mode 100644 index 0000000..d0c9e2d --- /dev/null +++ b/src/jexer/tterminal/DisplayListener.java @@ -0,0 +1,56 @@ +/* + * 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(); + +} diff --git a/src/jexer/tterminal/ECMA48.java b/src/jexer/tterminal/ECMA48.java new file mode 100644 index 0000000..1d34811 --- /dev/null +++ b/src/jexer/tterminal/ECMA48.java @@ -0,0 +1,7261 @@ +/* + * 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. + * + *

+ * It currently implements VT100, VT102, VT220, and XTERM with the following + * caveats: + * + *

+ * - 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. + * + *

+ * - Smooth scrolling, printing, keyboard locking, keyboard leds, and tests + * from VT100 are not supported. + * + *

+ * - 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.) + * + *

+ * - Numeric/application keys from the number pad are not supported because + * they are not exposed from the TKeypress API. + * + *

+ * - VT52 HOLD SCREEN mode is not supported. + * + *

+ * - In VT52 graphics mode, the 3/, 5/, and 7/ characters (fraction + * numerators) are not rendered correctly. + * + *

+ * - 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 scrollback; + + /** + * The raw display buffer characters + attributes. + */ + private volatile ArrayList 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 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 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 colors88; + + /** + * Sixel collection buffer. + */ + private StringBuilder sixelParseBuffer; + + /** + * Sixel shared palette. + */ + private HashMap 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 userQueue = new ArrayList(); + + /** + * 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(); + tabStops = new ArrayList(); + scrollback = new ArrayList(); + display = new ArrayList(); + + 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 getScrollbackBuffer() { + return scrollback; + } + + /** + * Get the display buffer. + * + * @return the display buffer + */ + public final List 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 getVisibleDisplay(final int visibleHeight, + final int scrollBottom) { + + assert (visibleHeight >= 0); + assert (scrollBottom >= 0); + + int visibleBottom = scrollback.size() + display.size() - scrollBottom; + + List preceedingBlankLines = new ArrayList(); + int visibleTop = visibleBottom - visibleHeight; + if (visibleTop < 0) { + for (int i = visibleTop; i < 0; i++) { + preceedingBlankLines.add(getBlankDisplayLine()); + } + visibleTop = 0; + } + assert (visibleTop >= 0); + + List displayLines = new ArrayList(); + displayLines.addAll(scrollback); + displayLines.addAll(display); + + List visibleLines = new ArrayList(); + 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 copyBuffer(final List buffer) { + ArrayList result = new ArrayList(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(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 displayTop = display.subList(0, regionTop); + List displayBottom = display.subList(regionBottom + 1, + display.size()); + List displayMiddle = display.subList(regionBottom + 1 + - remaining, regionBottom + 1); + display = new ArrayList(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 displayTop = display.subList(0, regionTop); + List displayBottom = display.subList(regionBottom + 1, + display.size()); + List displayMiddle = display.subList(regionTop, + regionTop + remaining); + display = new ArrayList(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(); + } + } + } + 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 newStops = new ArrayList(); + 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); + } + + } + +} diff --git a/src/jexer/tterminal/Sixel.java b/src/jexer/tterminal/Sixel.java new file mode 100644 index 0000000..a4c00fc --- /dev/null +++ b/src/jexer/tterminal/Sixel.java @@ -0,0 +1,589 @@ +/* + * 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 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 palette) { + this.buffer = buffer; + if (palette == null) { + this.palette = new HashMap(); + } 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; + + } + + } + +} diff --git a/src/jexer/tterminal/package-info.java b/src/jexer/tterminal/package-info.java new file mode 100644 index 0000000..b92d153 --- /dev/null +++ b/src/jexer/tterminal/package-info.java @@ -0,0 +1,33 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2019 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ + +/** + * An ECMA-48 / ANSI X3.64 style terminal emulator. + */ +package jexer.tterminal; diff --git a/src/jexer/ttree/TDirectoryTreeItem.java b/src/jexer/ttree/TDirectoryTreeItem.java new file mode 100644 index 0000000..9bdec01 --- /dev/null +++ b/src/jexer/ttree/TDirectoryTreeItem.java @@ -0,0 +1,211 @@ +/* + * 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 parentFiles = new LinkedList(); + 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()); + } + +} diff --git a/src/jexer/ttree/TTreeItem.java b/src/jexer/ttree/TTreeItem.java new file mode 100644 index 0000000..44c408b --- /dev/null +++ b/src/jexer/ttree/TTreeItem.java @@ -0,0 +1,483 @@ +/* + * 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 expandTree(final String prefix, final boolean last) { + List array = new ArrayList(); + 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(); + } + } + } + +} diff --git a/src/jexer/ttree/TTreeView.java b/src/jexer/ttree/TTreeView.java new file mode 100644 index 0000000..22f72ca --- /dev/null +++ b/src/jexer/ttree/TTreeView.java @@ -0,0 +1,329 @@ +/* + * 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()); + } + + } + +} diff --git a/src/jexer/ttree/TTreeViewWidget.java b/src/jexer/ttree/TTreeViewWidget.java new file mode 100644 index 0000000..080a200 --- /dev/null +++ b/src/jexer/ttree/TTreeViewWidget.java @@ -0,0 +1,406 @@ +/* + * 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(); + } + +} diff --git a/src/jexer/ttree/TTreeViewWindow.java b/src/jexer/ttree/TTreeViewWindow.java new file mode 100644 index 0000000..f418383 --- /dev/null +++ b/src/jexer/ttree/TTreeViewWindow.java @@ -0,0 +1,408 @@ +/* + * 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(); + } + +} diff --git a/src/jexer/ttree/package-info.java b/src/jexer/ttree/package-info.java new file mode 100644 index 0000000..1e1fdfd --- /dev/null +++ b/src/jexer/ttree/package-info.java @@ -0,0 +1,33 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2019 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ + +/** + * TTreeView and supporting classes. + */ +package jexer.ttree;