From: Kevin Lamonte Date: Sun, 3 Dec 2017 19:55:39 +0000 (-0500) Subject: PMD code sweep, #6 don't add MyWindow twice to MyApplication X-Git-Tag: fanfix-3.0.1^2~218 X-Git-Url: https://git.nikiroo.be/?a=commitdiff_plain;h=d36057dfab8def933a64be042b039d76708ac5ba;p=fanfix.git PMD code sweep, #6 don't add MyWindow twice to MyApplication --- diff --git a/README.md b/README.md index 1e2b7e9..dc97659 100644 --- a/README.md +++ b/README.md @@ -73,8 +73,9 @@ class MyApplication extends TApplication { addFileMenu(); addWindowMenu(); - // Add a custom window, see below for its code. - addWindow(new MyWindow(this)); + // Add a custom window, see below for its code. The TWindow + // constructor will add it to this application. + new MyWindow(this); } public static void main(String [] args) { diff --git a/build.xml b/build.xml index f21f321..4e7abb8 100644 --- a/build.xml +++ b/build.xml @@ -30,10 +30,6 @@ - - - @@ -94,7 +90,6 @@ version="true" use="true" access="protected" - failonwarning="true" windowtitle="Jexer - Java Text User Interface - API docs"> diff --git a/docs/worklog.md b/docs/worklog.md index f4a3a36..05d4f15 100644 --- a/docs/worklog.md +++ b/docs/worklog.md @@ -1,6 +1,17 @@ Jexer Work Log ============== +October 17, 2017 + +I finally gave up the ghost on using gcj as the default compiler due +to its awesome unused imports messages, and learned how to get PMD to +do that job. Which promptly created 1000+ warning messages related to +class item order (variables, constructors, methods), nested ifs, +useless checks, and so on. So now we go on a code sweep to fix those, +and along the way set a new class template. Since this is so large +and invasive, I will bite the bullet now and get it done before the +next release which will get it out on Maven finally. + August 16, 2017 Holy balls this has gotten so much faster! It is FINALLY visibly diff --git a/resources/jexer_logo_128.png b/resources/jexer_logo_128.png new file mode 100644 index 0000000..5c3a813 Binary files /dev/null and b/resources/jexer_logo_128.png differ diff --git a/src/jexer/TApplication.java b/src/jexer/TApplication.java index e61cea2..6dd503f 100644 --- a/src/jexer/TApplication.java +++ b/src/jexer/TApplication.java @@ -77,7 +77,7 @@ public class TApplication implements Runnable { private static final ResourceBundle i18n = ResourceBundle.getBundle(TApplication.class.getName()); // ------------------------------------------------------------------------ - // Public constants ------------------------------------------------------- + // Constants -------------------------------------------------------------- // ------------------------------------------------------------------------ /** @@ -117,9 +117,155 @@ public class TApplication implements Runnable { } // ------------------------------------------------------------------------ - // Primary/secondary event handlers --------------------------------------- + // Variables -------------------------------------------------------------- // ------------------------------------------------------------------------ + /** + * The primary event handler thread. + */ + private volatile WidgetEventHandler primaryEventHandler; + + /** + * The secondary event handler thread. + */ + private volatile WidgetEventHandler secondaryEventHandler; + + /** + * 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; + + /** + * 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 static final 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; + /** * WidgetEventHandler is the main event consumer loop. There are at most * two such threads in existence: the primary for normal case and a @@ -264,1163 +410,1153 @@ public class TApplication implements Runnable { } } - /** - * The primary event handler thread. - */ - private volatile WidgetEventHandler primaryEventHandler; + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ /** - * The secondary event handler thread. + * 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 */ - private volatile WidgetEventHandler secondaryEventHandler; + public TApplication(final BackendType backendType, final int windowWidth, + final int windowHeight, final int fontSize) + throws UnsupportedEncodingException { - /** - * The widget receiving events from the secondary event handler thread. - */ - private volatile TWidget secondaryEventReceiver; + 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(); + } /** - * Wake the sleeping active event handler. + * Public constructor. + * + * @param backendType BackendType.XTERM, BackendType.ECMA48 or + * BackendType.SWING + * @throws UnsupportedEncodingException if an exception is thrown when + * creating the InputStreamReader */ - private void wakeEventHandler() { - if (!started) { - return; - } + public TApplication(final BackendType backendType) + throws UnsupportedEncodingException { - if (secondaryEventHandler != null) { - synchronized (secondaryEventHandler) { - secondaryEventHandler.notify(); - } - } else { - assert (primaryEventHandler != null); - synchronized (primaryEventHandler) { - primaryEventHandler.notify(); - } + 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(); } - // ------------------------------------------------------------------------ - // TApplication attributes ------------------------------------------------ - // ------------------------------------------------------------------------ - /** - * Access to the physical screen, keyboard, and mouse. - */ - private Backend backend; - - /** - * Get the Backend. + * Public constructor. The backend type will be BackendType.ECMA48. * - * @return the Backend + * @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 final Backend getBackend() { - return backend; + public TApplication(final InputStream input, + final OutputStream output) throws UnsupportedEncodingException { + + backend = new ECMA48Backend(this, input, output); + TApplicationImpl(); } /** - * Get the Screen. + * Public constructor. The backend type will be BackendType.ECMA48. * - * @return the Screen + * @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 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(); - } + public TApplication(final InputStream input, final Reader reader, + final PrintWriter writer, final boolean setRawMode) { + + backend = new ECMA48Backend(this, input, reader, writer, setRawMode); + TApplicationImpl(); } /** - * Actual mouse coordinate X. + * 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. */ - private int mouseX; + public TApplication(final InputStream input, final Reader reader, + final PrintWriter writer) { - /** - * Actual mouse coordinate Y. - */ - private int mouseY; + this(input, reader, writer, false); + } /** - * Old version of mouse coordinate X. + * Public constructor. This hook enables use with new non-Jexer + * backends. + * + * @param backend a Backend that is already ready to go. */ - private int oldMouseX; + public TApplication(final Backend backend) { + this.backend = backend; + backend.setListener(this); + TApplicationImpl(); + } /** - * Old version mouse coordinate Y. + * Finish construction once the backend is set. */ - private int oldMouseY; + private void TApplicationImpl() { + theme = new ColorTheme(); + desktopBottom = getScreen().getHeight() - 1; + fillEventQueue = new ArrayList(); + drainEventQueue = new ArrayList(); + windows = new LinkedList(); + menus = new LinkedList(); + subMenus = new LinkedList(); + timers = new LinkedList(); + accelerators = new HashMap(); + menuItems = new ArrayList(); + desktop = new TDesktop(this); - /** - * The last mouse up click time, used to determine if this is a mouse - * double-click. - */ - private long lastMouseUpTime; + // 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(); + } + } + ); + } + } + } - /** - * The amount of millis between mouse up events to assume a double-click. - */ - private long doubleClickTime = 250; + // ------------------------------------------------------------------------ + // Runnable --------------------------------------------------------------- + // ------------------------------------------------------------------------ /** - * Event queue that is filled by run(). + * Run this application until it exits. */ - private List fillEventQueue; + public void run() { + // Start the main consumer thread + primaryEventHandler = new WidgetEventHandler(this, true); + (new Thread(primaryEventHandler)).start(); - /** - * Event queue that will be drained by either primary or secondary - * Thread. - */ - private List drainEventQueue; + started = true; - /** - * Top-level menus in this application. - */ - private List menus; + while (!quit) { + synchronized (this) { + boolean doWait = false; - /** - * Stack of activated sub-menus in this application. - */ - private List subMenus; + if (!backend.hasEvents()) { + synchronized (fillEventQueue) { + if (fillEventQueue.size() == 0) { + doWait = true; + } + } + } - /** - * The currently active menu. - */ - private TMenu activeMenu = null; + if (doWait) { + // No I/O to dispatch, so wait until the backend + // provides new I/O. + try { + if (debugThreads) { + System.err.println(System.currentTimeMillis() + + " MAIN sleep"); + } - /** - * Active keyboard accelerators. - */ - private Map accelerators; + this.wait(); - /** - * All menu items. - */ - private List menuItems; + if (debugThreads) { + System.err.println(System.currentTimeMillis() + + " MAIN AWAKE"); + } + } catch (InterruptedException e) { + // I'm awake and don't care why, let's see what's + // going on out there. + } + } - /** - * Windows and widgets pull colors from this ColorTheme. - */ - private ColorTheme theme; + } // synchronized (this) - /** - * Get the color theme. - * - * @return the theme - */ - public final ColorTheme getTheme() { - return theme; - } + synchronized (fillEventQueue) { + // Pull any pending I/O events + backend.getEvents(fillEventQueue); - /** - * The top-level windows (but not menus). - */ - private List windows; + // 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); + } + } - /** - * The currently acive window. - */ - private TWindow activeWindow = null; + // Wake a consumer thread if we have any pending events. + if (drainEventQueue.size() > 0) { + wakeEventHandler(); + } - /** - * Timers that are being ticked. - */ - private List timers; + } // while (!quit) - /** - * When true, the application has been started. - */ - private volatile boolean started = false; + // Shutdown the event consumer threads + if (secondaryEventHandler != null) { + synchronized (secondaryEventHandler) { + secondaryEventHandler.notify(); + } + } + if (primaryEventHandler != null) { + synchronized (primaryEventHandler) { + primaryEventHandler.notify(); + } + } - /** - * When true, exit the application. - */ - private volatile boolean quit = false; + // Shutdown the user I/O thread(s) + backend.shutdown(); - /** - * When true, repaint the entire screen. - */ - private volatile boolean repaint = true; + // Close all the windows. This gives them an opportunity to release + // resources. + closeAllWindows(); - /** - * Repaint the screen on the next update. - */ - public void doRepaint() { - repaint = true; - wakeEventHandler(); } - /** - * 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 static final int desktopTop = 1; + // ------------------------------------------------------------------------ + // Event handlers --------------------------------------------------------- + // ------------------------------------------------------------------------ /** - * Get Y coordinate of the top edge of the desktop. + * Method that TApplication subclasses can override to handle menu or + * posted command events. * - * @return Y coordinate of the top edge of the desktop + * @param command command event + * @return if true, this event was consumed */ - public final int getDesktopTop() { - return desktopTop; - } - - /** - * Y coordinate of the bottom edge of the desktop. - */ - private int desktopBottom; + protected boolean onCommand(final TCommandEvent command) { + // Default: handle cmExit + if (command.equals(cmExit)) { + if (messageBox(i18n.getString("exitDialogTitle"), + i18n.getString("exitDialogText"), + TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) { + exit(); + } + return true; + } - /** - * 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; - } + if (command.equals(cmShell)) { + openTerminal(0, 0, TWindow.RESIZABLE); + return true; + } - /** - * An optional TDesktop background window that is drawn underneath - * everything else. - */ - private TDesktop desktop; + if (command.equals(cmTile)) { + tileWindows(); + return true; + } + if (command.equals(cmCascade)) { + cascadeWindows(); + return true; + } + if (command.equals(cmCloseAll)) { + closeAllWindows(); + return true; + } - /** - * 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.onClose(); + if (command.equals(cmMenu)) { + if (!modalWindowActive() && (activeMenu == null)) { + if (menus.size() > 0) { + menus.get(0).setActive(true); + activeMenu = menus.get(0); + return true; + } + } } - this.desktop = desktop; - } - /** - * Get the TDesktop instance. - * - * @return the desktop, or null if it is not set - */ - public final TDesktop getDesktop() { - return desktop; + return false; } /** - * Get the current active window. + * Method that TApplication subclasses can override to handle menu + * events. * - * @return the active window, or null if it is not set + * @param menu menu event + * @return if true, this event was consumed */ - public final TWindow getActiveWindow() { - return activeWindow; - } + protected boolean onMenu(final TMenuEvent menu) { - /** - * 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 LinkedList(); - result.addAll(windows); - return result; - } + // Default: handle MID_EXIT + if (menu.getId() == TMenu.MID_EXIT) { + if (messageBox(i18n.getString("exitDialogTitle"), + i18n.getString("exitDialogText"), + TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) { + exit(); + } + return true; + } - /** - * If true, focus follows mouse: windows automatically raised if the - * mouse passes over them. - */ - private boolean focusFollowsMouse = false; + if (menu.getId() == TMenu.MID_SHELL) { + openTerminal(0, 0, TWindow.RESIZABLE); + return true; + } - /** - * Get focusFollowsMouse flag. - * - * @return true if focus follows mouse: windows automatically raised if - * the mouse passes over them - */ - public boolean getFocusFollowsMouse() { - return focusFollowsMouse; + 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) { + doRepaint(); + return true; + } + return false; } /** - * Set focusFollowsMouse flag. + * Method that TApplication subclasses can override to handle keystrokes. * - * @param focusFollowsMouse if true, focus follows mouse: windows - * automatically raised if the mouse passes over them + * @param keypress keystroke event + * @return if true, this event was consumed */ - public void setFocusFollowsMouse(final boolean focusFollowsMouse) { - this.focusFollowsMouse = focusFollowsMouse; - } + protected boolean onKeypress(final TKeypressEvent keypress) { + // Default: only menu shortcuts - // ------------------------------------------------------------------------ - // General behavior ------------------------------------------------------- - // ------------------------------------------------------------------------ + // Process Alt-F, Alt-E, etc. menu shortcut keys + if (!keypress.getKey().isFnKey() + && keypress.getKey().isAlt() + && !keypress.getKey().isCtrl() + && (activeMenu == null) + && !modalWindowActive() + ) { - /** - * Display the about dialog. - */ - protected void showAboutDialog() { - messageBox(i18n.getString("aboutDialogTitle"), - MessageFormat.format(i18n.getString("aboutDialogText"), - this.getClass().getPackage().getImplementationVersion()), - TMessageBox.Type.OK); - } + assert (subMenus.size() == 0); - // ------------------------------------------------------------------------ - // Constructors ----------------------------------------------------------- - // ------------------------------------------------------------------------ + for (TMenu menu: menus) { + if (Character.toLowerCase(menu.getMnemonic().getShortcut()) + == Character.toLowerCase(keypress.getKey().getChar()) + ) { + activeMenu = menu; + menu.setActive(true); + return true; + } + } + } + + return false; + } /** - * 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 + * Process background events, and update the screen. */ - public TApplication(final BackendType backendType, final int windowWidth, - final int windowHeight, final int fontSize) - throws UnsupportedEncodingException { + private void finishEventProcessing() { + if (debugThreads) { + System.err.printf(System.currentTimeMillis() + " " + + Thread.currentThread() + " finishEventProcessing()\n"); + } - 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); + // Process timers and call doIdle()'s + doIdle(); + + // Update the screen + synchronized (getScreen()) { + drawAll(); + } + + if (debugThreads) { + System.err.printf(System.currentTimeMillis() + " " + + Thread.currentThread() + " finishEventProcessing() END\n"); } - TApplicationImpl(); } /** - * Public constructor. + * Peek at certain application-level events, add to eventQueue, and wake + * up the consuming Thread. * - * @param backendType BackendType.XTERM, BackendType.ECMA48 or - * BackendType.SWING - * @throws UnsupportedEncodingException if an exception is thrown when - * creating the InputStreamReader + * @param event the input event to consume */ - public TApplication(final BackendType backendType) - throws UnsupportedEncodingException { + private void metaHandleEvent(final TInputEvent event) { - 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); + if (debugEvents) { + System.err.printf(String.format("metaHandleEvents event: %s\n", + event)); System.err.flush(); } - 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(); - } + if (quit) { + // Do no more processing if the application is already trying + // to exit. + return; + } - /** - * Finish construction once the backend is set. - */ - private void TApplicationImpl() { - theme = new ColorTheme(); - desktopBottom = getScreen().getHeight() - 1; - fillEventQueue = new ArrayList(); - drainEventQueue = new ArrayList(); - windows = new LinkedList(); - menus = new LinkedList(); - subMenus = new LinkedList(); - timers = new LinkedList(); - accelerators = new HashMap(); - menuItems = new ArrayList(); - desktop = new TDesktop(this); + // Special application-wide events ------------------------------- - // 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(); - } - } - ); + // Abort everything + if (event instanceof TCommandEvent) { + TCommandEvent command = (TCommandEvent) event; + if (command.getCmd().equals(cmAbort)) { + exit(); + return; } } - } - // ------------------------------------------------------------------------ - // Screen refresh loop ---------------------------------------------------- - // ------------------------------------------------------------------------ + synchronized (drainEventQueue) { + // Screen resize + if (event instanceof TResizeEvent) { + TResizeEvent resize = (TResizeEvent) event; + synchronized (getScreen()) { + getScreen().setDimensions(resize.getWidth(), + resize.getHeight()); + desktopBottom = getScreen().getHeight() - 1; + mouseX = 0; + mouseY = 0; + oldMouseX = 0; + oldMouseY = 0; + } + if (desktop != null) { + desktop.setDimensions(0, 0, resize.getWidth(), + resize.getHeight() - 1); + } - /** - * Process background events, and update the screen. - */ - private void finishEventProcessing() { - if (debugThreads) { - System.err.printf(System.currentTimeMillis() + " " + - Thread.currentThread() + " finishEventProcessing()\n"); - } + // Change menu edges if needed. + recomputeMenuX(); - // Process timers and call doIdle()'s - doIdle(); + // We are dirty, redraw the screen. + doRepaint(); - // Update the screen - synchronized (getScreen()) { - drawAll(); - } + /* + System.err.println("New screen: " + resize.getWidth() + + " x " + resize.getHeight()); + */ + return; + } - if (debugThreads) { - System.err.printf(System.currentTimeMillis() + " " + - Thread.currentThread() + " finishEventProcessing() END\n"); + // Put into the main queue + drainEventQueue.add(event); } } /** - * Invert the cell color at a position. This is used to track the mouse. + * 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 x column position - * @param y row position + * @param event the input event to consume + * @see #secondaryHandleEvent(TInputEvent event) */ - private void invertCell(final int x, final int y) { - if (debugThreads) { - System.err.printf("%d %s invertCell() %d %d\n", - System.currentTimeMillis(), Thread.currentThread(), x, y); + private void primaryHandleEvent(final TInputEvent event) { + + if (debugEvents) { + System.err.printf("Handle event: %s\n", event); } - CellAttributes attr = getScreen().getAttrXY(x, y); - attr.setForeColor(attr.getForeColor().invert()); - attr.setBackColor(attr.getBackColor().invert()); - getScreen().putAttrXY(x, y, attr, false); - } + TMouseEvent doubleClick = null; - /** - * Draw everything. - */ - private void drawAll() { - boolean menuIsActive = false; + // Special application-wide events ----------------------------------- - if (debugThreads) { - System.err.printf("%d %s drawAll() enter\n", - System.currentTimeMillis(), Thread.currentThread()); - } + // Peek at the mouse position + if (event instanceof TMouseEvent) { + 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_UP) { + if ((mouse.getTime().getTime() - lastMouseUpTime) < + doubleClickTime) { - if (!repaint) { - if (debugThreads) { - System.err.printf("%d %s drawAll() !repaint\n", - System.currentTimeMillis(), Thread.currentThread()); - } - synchronized (getScreen()) { - if ((oldMouseX != mouseX) || (oldMouseY != mouseY)) { - // The only thing that has happened is the mouse moved. - // Clear the old position and draw the new position. - invertCell(oldMouseX, oldMouseY); - invertCell(mouseX, mouseY); - oldMouseX = mouseX; - oldMouseY = mouseY; - } - if (getScreen().isDirty()) { - backend.flushScreen(); + // 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(); + } } - return; } - } - if (debugThreads) { - System.err.printf("%d %s drawAll() REDRAW\n", - System.currentTimeMillis(), Thread.currentThread()); + // See if we need to switch focus to another window or the menu + checkSwitchFocus((TMouseEvent) event); } - // If true, the cursor is not visible - boolean cursor = false; - - // Start with a clean screen - getScreen().clear(); + // Handle menu events + if ((activeMenu != null) && !(event instanceof TCommandEvent)) { + TMenu menu = activeMenu; - // Draw the desktop - if (desktop != null) { - desktop.drawChildren(); - } + if (event instanceof TMouseEvent) { + TMouseEvent mouse = (TMouseEvent) event; - // Draw each window in reverse Z order - List sorted = new LinkedList(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(); - } - } - - // 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, menu.getTitle().length() + 2, ' ', - menuColor); - getScreen().putStringXY(x + 1, 0, menu.getTitle(), menuColor); - // Draw the highlight character - getScreen().putCharXY(x + 1 + menu.getMnemonic().getShortcutIdx(), - 0, menu.getMnemonic().getShortcut(), menuMnemonicColor); + 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(); + } - if (menu.isActive()) { - menu.drawChildren(); - // Reset the screen clipping so we can draw the next title. - getScreen().resetClipping(); + // 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()); } - x += menu.getTitle().length() + 2; - } - - for (TMenu menu: subMenus) { - // Reset the screen clipping so we can draw the next sub-menu. - getScreen().resetClipping(); - menu.drawChildren(); + menu.handleEvent(event); + return; } - // 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); - } + if (event instanceof TKeypressEvent) { + TKeypressEvent keypress = (TKeypressEvent) event; - // Draw the mouse pointer - invertCell(mouseX, mouseY); - oldMouseX = mouseX; - oldMouseY = mouseY; + // 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; + } + } - // Place the cursor if it is visible - if (!menuIsActive) { - TWidget activeWidget = null; - if (sorted.size() > 0) { - activeWidget = sorted.get(sorted.size() - 1).getActiveChild(); - if (activeWidget.isCursorVisible()) { - if ((activeWidget.getCursorAbsoluteY() < desktopBottom) - && (activeWidget.getCursorAbsoluteY() > desktopTop) - ) { - getScreen().putCursor(true, - activeWidget.getCursorAbsoluteX(), - activeWidget.getCursorAbsoluteY()); - cursor = true; - } else { - getScreen().putCursor(false, - activeWidget.getCursorAbsoluteX(), - activeWidget.getCursorAbsoluteY()); - cursor = false; + 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; + } } } - // Kill the cursor - if (!cursor) { - getScreen().hideCursor(); + if (event instanceof TCommandEvent) { + if (onCommand((TCommandEvent) event)) { + return; + } } - // Flush the screen contents - if (getScreen().isDirty()) { - backend.flushScreen(); + if (event instanceof TMenuEvent) { + if (onMenu((TMenuEvent) event)) { + return; + } } - repaint = false; - } + // 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()); - // ------------------------------------------------------------------------ - // Main loop -------------------------------------------------------------- - // ------------------------------------------------------------------------ + if (doubleClick != null) { + doubleClick.setX(doubleClick.getX() - window.getX()); + doubleClick.setY(doubleClick.getY() - window.getY()); + } - /** - * Force this application to exit. - */ - public void exit() { - quit = true; - synchronized (this) { - this.notify(); + if (window.mouseWouldHit(mouse)) { + dispatchToDesktop = false; + } + } else if (event instanceof TKeypressEvent) { + 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); + } + } } } /** - * Run this application until it exits. + * 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) */ - public void run() { - // Start the main consumer thread - primaryEventHandler = new WidgetEventHandler(this, true); - (new Thread(primaryEventHandler)).start(); + private void secondaryHandleEvent(final TInputEvent event) { + TMouseEvent doubleClick = null; - started = true; + // Peek at the mouse position + if (event instanceof TMouseEvent) { + 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_UP) { + if ((mouse.getTime().getTime() - lastMouseUpTime) < + doubleClickTime) { - while (!quit) { - synchronized (this) { - boolean doWait = false; + // 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()); - synchronized (fillEventQueue) { - if (fillEventQueue.size() == 0) { - doWait = true; + } else { + // The first click of a potential double-click. + lastMouseUpTime = mouse.getTime().getTime(); } } + } + } - if (doWait) { - // No I/O to dispatch, so wait until the backend - // provides new I/O. - try { - if (debugThreads) { - System.err.println(System.currentTimeMillis() + - " MAIN sleep"); - } + secondaryEventReceiver.handleEvent(event); + if (doubleClick != null) { + secondaryEventReceiver.handleEvent(doubleClick); + } + } - this.wait(); + /** + * 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()"); + } - if (debugThreads) { - System.err.println(System.currentTimeMillis() + - " MAIN AWAKE"); - } - } catch (InterruptedException e) { - // I'm awake and don't care why, let's see what's - // going on out there. - } + 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() { + assert (secondaryEventReceiver != null); + + while (secondaryEventReceiver != null) { + synchronized (primaryEventHandler) { + try { + primaryEventHandler.wait(); + } catch (InterruptedException e) { + // SQUASH } + } + } + } - } // synchronized (this) + /** + * Do stuff when there is no user input. + */ + private void doIdle() { + if (debugThreads) { + System.err.printf(System.currentTimeMillis() + " " + + Thread.currentThread() + " doIdle()\n"); + } - synchronized (fillEventQueue) { - // Pull any pending I/O events - backend.getEvents(fillEventQueue); + synchronized (timers) { - // Dispatch each event to the appropriate handler, one at a - // time. - for (;;) { - TInputEvent event = null; - if (fillEventQueue.size() == 0) { - break; + 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); } - event = fillEventQueue.remove(0); - metaHandleEvent(event); + } else { + keepTimers.add(timer); } } + timers = keepTimers; + } - // Wake a consumer thread if we have any pending events. - if (drainEventQueue.size() > 0) { - wakeEventHandler(); - } + // Call onIdle's + for (TWindow window: windows) { + window.onIdle(); + } + if (desktop != null) { + desktop.onIdle(); + } + } - } // while (!quit) + /** + * Wake the sleeping active event handler. + */ + private void wakeEventHandler() { + if (!started) { + return; + } - // Shutdown the event consumer threads if (secondaryEventHandler != null) { synchronized (secondaryEventHandler) { secondaryEventHandler.notify(); } - } - if (primaryEventHandler != null) { + } else { + assert (primaryEventHandler != null); synchronized (primaryEventHandler) { primaryEventHandler.notify(); } } + } - // Shutdown the user I/O thread(s) - backend.shutdown(); - - // Close all the windows. This gives them an opportunity to release - // resources. - closeAllWindows(); + // ------------------------------------------------------------------------ + // TApplication ----------------------------------------------------------- + // ------------------------------------------------------------------------ + /** + * Get the Backend. + * + * @return the Backend + */ + public final Backend getBackend() { + return backend; } /** - * Peek at certain application-level events, add to eventQueue, and wake - * up the consuming Thread. + * Get the Screen. * - * @param event the input event to consume + * @return the Screen */ - 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; + 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(); } + } - // Special application-wide events ------------------------------- - - // Abort everything - if (event instanceof TCommandEvent) { - TCommandEvent command = (TCommandEvent) event; - if (command.getCmd().equals(cmAbort)) { - exit(); - return; - } - } + /** + * Get the color theme. + * + * @return the theme + */ + public final ColorTheme getTheme() { + return theme; + } - synchronized (drainEventQueue) { - // Screen resize - if (event instanceof TResizeEvent) { - TResizeEvent resize = (TResizeEvent) event; - synchronized (getScreen()) { - getScreen().setDimensions(resize.getWidth(), - resize.getHeight()); - desktopBottom = getScreen().getHeight() - 1; - mouseX = 0; - mouseY = 0; - oldMouseX = 0; - oldMouseY = 0; - } - if (desktop != null) { - desktop.setDimensions(0, 0, resize.getWidth(), - resize.getHeight() - 1); - } + /** + * Repaint the screen on the next update. + */ + public void doRepaint() { + repaint = true; + wakeEventHandler(); + } - // Change menu edges if needed. - recomputeMenuX(); + /** + * 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; + } - // We are dirty, redraw the screen. - doRepaint(); - return; - } + /** + * 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; + } - // Put into the main queue - drainEventQueue.add(event); + /** + * 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.onClose(); } + this.desktop = desktop; } /** - * 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. + * Get the TDesktop instance. * - * @param event the input event to consume - * @see #secondaryHandleEvent(TInputEvent event) + * @return the desktop, or null if it is not set */ - private void primaryHandleEvent(final TInputEvent event) { - - if (debugEvents) { - System.err.printf("Handle event: %s\n", event); - } - TMouseEvent doubleClick = null; + public final TDesktop getDesktop() { + return desktop; + } - // Special application-wide events ----------------------------------- + /** + * Get the current active window. + * + * @return the active window, or null if it is not set + */ + public final TWindow getActiveWindow() { + return activeWindow; + } - // Peek at the mouse position - if (event instanceof TMouseEvent) { - 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_UP) { - if ((mouse.getTime().getTime() - lastMouseUpTime) < - doubleClickTime) { + /** + * 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 LinkedList(); + result.addAll(windows); + return result; + } - // 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()); + /** + * Get focusFollowsMouse flag. + * + * @return true if focus follows mouse: windows automatically raised if + * the mouse passes over them + */ + public boolean getFocusFollowsMouse() { + return focusFollowsMouse; + } - } else { - // The first click of a potential double-click. - lastMouseUpTime = mouse.getTime().getTime(); - } - } - } + /** + * 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; + } - // See if we need to switch focus to another window or the menu - checkSwitchFocus((TMouseEvent) event); - } + /** + * Display the about dialog. + */ + protected void showAboutDialog() { + messageBox(i18n.getString("aboutDialogTitle"), + MessageFormat.format(i18n.getString("aboutDialogText"), + this.getClass().getPackage().getImplementationVersion()), + TMessageBox.Type.OK); + } - // Handle menu events - if ((activeMenu != null) && !(event instanceof TCommandEvent)) { - TMenu menu = activeMenu; + // ------------------------------------------------------------------------ + // Screen refresh loop ---------------------------------------------------- + // ------------------------------------------------------------------------ - if (event instanceof TMouseEvent) { - TMouseEvent mouse = (TMouseEvent) event; + /** + * 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) { + if (debugThreads) { + System.err.printf("%d %s invertCell() %d %d\n", + System.currentTimeMillis(), Thread.currentThread(), x, y); + } + CellAttributes attr = getScreen().getAttrXY(x, y); + attr.setForeColor(attr.getForeColor().invert()); + attr.setBackColor(attr.getBackColor().invert()); + getScreen().putAttrXY(x, y, attr, false); + } - 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(); - } + /** + * Draw everything. + */ + private void drawAll() { + boolean menuIsActive = false; - // 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 (debugThreads) { + System.err.printf("%d %s drawAll() enter\n", + System.currentTimeMillis(), Thread.currentThread()); } - 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 (!repaint) { + if (debugThreads) { + System.err.printf("%d %s drawAll() !repaint\n", + System.currentTimeMillis(), Thread.currentThread()); } - - 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; - } + synchronized (getScreen()) { + if ((oldMouseX != mouseX) || (oldMouseY != mouseY)) { + // The only thing that has happened is the mouse moved. + // Clear the old position and draw the new position. + invertCell(oldMouseX, oldMouseY); + invertCell(mouseX, mouseY); + oldMouseX = mouseX; + oldMouseY = mouseY; } - - // Handle the keypress - if (onKeypress(keypress)) { - return; + if (getScreen().isDirty()) { + backend.flushScreen(); } - } - } - - if (event instanceof TCommandEvent) { - if (onCommand((TCommandEvent) event)) { return; } } - if (event instanceof TMenuEvent) { - if (onMenu((TMenuEvent) event)) { - return; - } + if (debugThreads) { + System.err.printf("%d %s drawAll() REDRAW\n", + System.currentTimeMillis(), Thread.currentThread()); } - // 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 true, the cursor is not visible + boolean cursor = false; - if (doubleClick != null) { - doubleClick.setX(doubleClick.getX() - window.getX()); - doubleClick.setY(doubleClick.getY() - window.getY()); - } + // Start with a clean screen + getScreen().clear(); - if (window.mouseWouldHit(mouse)) { - dispatchToDesktop = false; - } - } else if (event instanceof TKeypressEvent) { - dispatchToDesktop = false; - } + // Draw the desktop + if (desktop != null) { + desktop.drawChildren(); + } - if (debugEvents) { - System.err.printf("TApplication dispatch event: %s\n", - event); - } - window.handleEvent(event); - if (doubleClick != null) { - window.handleEvent(doubleClick); + // Draw each window in reverse Z order + List sorted = new LinkedList(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 (dispatchToDesktop) { - // This event is fair game for the desktop to process. - if (desktop != null) { - desktop.handleEvent(event); - if (doubleClick != null) { - desktop.handleEvent(doubleClick); - } + + // 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, menu.getTitle().length() + 2, ' ', + menuColor); + getScreen().putStringXY(x + 1, 0, menu.getTitle(), menuColor); + // Draw the highlight character + getScreen().putCharXY(x + 1 + menu.getMnemonic().getShortcutIdx(), + 0, menu.getMnemonic().getShortcut(), menuMnemonicColor); + + if (menu.isActive()) { + menu.drawChildren(); + // Reset the screen clipping so we can draw the next title. + getScreen().resetClipping(); } + x += menu.getTitle().length() + 2; } - } - /** - * 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; + for (TMenu menu: subMenus) { + // Reset the screen clipping so we can draw the next sub-menu. + getScreen().resetClipping(); + menu.drawChildren(); + } - // Peek at the mouse position - if (event instanceof TMouseEvent) { - 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_UP) { - if ((mouse.getTime().getTime() - lastMouseUpTime) < - doubleClickTime) { + // 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); + } - // 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()); + // Draw the mouse pointer + invertCell(mouseX, mouseY); + oldMouseX = mouseX; + oldMouseY = mouseY; + // Place the cursor if it is visible + if (!menuIsActive) { + TWidget activeWidget = null; + if (sorted.size() > 0) { + activeWidget = sorted.get(sorted.size() - 1).getActiveChild(); + if (activeWidget.isCursorVisible()) { + if ((activeWidget.getCursorAbsoluteY() < desktopBottom) + && (activeWidget.getCursorAbsoluteY() > desktopTop) + ) { + getScreen().putCursor(true, + activeWidget.getCursorAbsoluteX(), + activeWidget.getCursorAbsoluteY()); + cursor = true; } else { - // The first click of a potential double-click. - lastMouseUpTime = mouse.getTime().getTime(); + getScreen().putCursor(false, + activeWidget.getCursorAbsoluteX(), + activeWidget.getCursorAbsoluteY()); + cursor = false; } } } } - secondaryEventReceiver.handleEvent(event); - if (doubleClick != null) { - secondaryEventReceiver.handleEvent(doubleClick); + // Kill the cursor + if (!cursor) { + getScreen().hideCursor(); } - } - /** - * 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()"); + // Flush the screen contents + if (getScreen().isDirty()) { + backend.flushScreen(); } - 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() { - assert (secondaryEventReceiver != null); - - while (secondaryEventReceiver != null) { - synchronized (primaryEventHandler) { - try { - primaryEventHandler.wait(); - } catch (InterruptedException e) { - // SQUASH - } - } - } + repaint = false; } /** - * Do stuff when there is no user input. + * Force this application to exit. */ - 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 = keepTimers; - } - - // Call onIdle's - for (TWindow window: windows) { - window.onIdle(); - } - if (desktop != null) { - desktop.onIdle(); + public void exit() { + quit = true; + synchronized (this) { + this.notify(); } } @@ -1770,7 +1906,7 @@ public class TApplication implements Runnable { * * @param window new window to add */ - public final void addWindow(final TWindow window) { + public final void addWindowToApplication(final TWindow window) { // Do not add menu windows to the window list. if (window instanceof TMenu) { @@ -1816,7 +1952,9 @@ public class TApplication implements Runnable { } if (((window.flags & TWindow.CENTERED) == 0) - && smartWindowPlacement) { + && ((window.flags & TWindow.ABSOLUTEXY) == 0) + && (smartWindowPlacement == true) + ) { doSmartPlacement(window); } @@ -2577,139 +2715,6 @@ public class TApplication implements Runnable { return helpMenu; } - // ------------------------------------------------------------------------ - // 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).getResult() == TMessageBox.Result.YES) { - 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)) { - 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).getResult() == TMessageBox.Result.YES) { - 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) { - doRepaint(); - 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() - ) { - - 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; - } - // ------------------------------------------------------------------------ // TTimer management ------------------------------------------------------ // ------------------------------------------------------------------------ diff --git a/src/jexer/TButton.java b/src/jexer/TButton.java index 255dd99..d4e7c89 100644 --- a/src/jexer/TButton.java +++ b/src/jexer/TButton.java @@ -44,20 +44,15 @@ import static jexer.TKeypress.*; */ public final class TButton extends TWidget { + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * The shortcut and button text. */ private MnemonicString mnemonic; - /** - * Get the mnemonic string for this button. - * - * @return mnemonic string - */ - public MnemonicString getMnemonic() { - return mnemonic; - } - /** * Remember mouse state. */ @@ -73,16 +68,9 @@ public final class TButton extends TWidget { */ private TAction action; - /** - * 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(); - inButtonPress = false; - } - } + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ /** * Private constructor. @@ -122,6 +110,10 @@ public final class TButton extends TWidget { this.action = action; } + // ------------------------------------------------------------------------ + // Event handlers --------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Returns true if the mouse is currently on the button. * @@ -142,55 +134,6 @@ public final class TButton extends TWidget { return false; } - /** - * Draw a button with a shadow. - */ - @Override - public void draw() { - CellAttributes buttonColor; - CellAttributes menuMnemonicColor; - CellAttributes shadowColor = new CellAttributes(); - shadowColor.setTo(getWindow().getBackground()); - shadowColor.setForeColor(Color.BLACK); - shadowColor.setBold(false); - - 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) { - getScreen().putCharXY(1, 0, ' ', buttonColor); - getScreen().putStringXY(2, 0, mnemonic.getRawLabel(), buttonColor); - getScreen().putCharXY(getWidth() - 1, 0, ' ', buttonColor); - } else { - getScreen().putCharXY(0, 0, ' ', buttonColor); - getScreen().putStringXY(1, 0, mnemonic.getRawLabel(), buttonColor); - getScreen().putCharXY(getWidth() - 2, 0, ' ', buttonColor); - - getScreen().putCharXY(getWidth() - 1, 0, - GraphicsChars.CP437[0xDC], shadowColor); - getScreen().hLineXY(1, 1, getWidth() - 1, - GraphicsChars.CP437[0xDF], shadowColor); - } - if (mnemonic.getShortcutIdx() >= 0) { - if (inButtonPress) { - getScreen().putCharXY(2 + mnemonic.getShortcutIdx(), 0, - mnemonic.getShortcut(), menuMnemonicColor); - } else { - getScreen().putCharXY(1 + mnemonic.getShortcutIdx(), 0, - mnemonic.getShortcut(), menuMnemonicColor); - } - - } - } - /** * Handle mouse button presses. * @@ -255,4 +198,81 @@ public final class TButton extends TWidget { super.onKeypress(keypress); } + // ------------------------------------------------------------------------ + // TWidget ---------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Draw a button with a shadow. + */ + @Override + public void draw() { + CellAttributes buttonColor; + CellAttributes menuMnemonicColor; + CellAttributes shadowColor = new CellAttributes(); + shadowColor.setTo(getWindow().getBackground()); + shadowColor.setForeColor(Color.BLACK); + shadowColor.setBold(false); + + 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) { + getScreen().putCharXY(1, 0, ' ', buttonColor); + getScreen().putStringXY(2, 0, mnemonic.getRawLabel(), buttonColor); + getScreen().putCharXY(getWidth() - 1, 0, ' ', buttonColor); + } else { + getScreen().putCharXY(0, 0, ' ', buttonColor); + getScreen().putStringXY(1, 0, mnemonic.getRawLabel(), buttonColor); + getScreen().putCharXY(getWidth() - 2, 0, ' ', buttonColor); + + getScreen().putCharXY(getWidth() - 1, 0, + GraphicsChars.CP437[0xDC], shadowColor); + getScreen().hLineXY(1, 1, getWidth() - 1, + GraphicsChars.CP437[0xDF], shadowColor); + } + if (mnemonic.getShortcutIdx() >= 0) { + if (inButtonPress) { + getScreen().putCharXY(2 + mnemonic.getShortcutIdx(), 0, + mnemonic.getShortcut(), menuMnemonicColor); + } else { + getScreen().putCharXY(1 + mnemonic.getShortcutIdx(), 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(); + inButtonPress = false; + } + } + } diff --git a/src/jexer/TCheckbox.java b/src/jexer/TCheckbox.java index 78c83c8..86933a7 100644 --- a/src/jexer/TCheckbox.java +++ b/src/jexer/TCheckbox.java @@ -39,34 +39,24 @@ import jexer.event.TMouseEvent; */ public final class TCheckbox extends TWidget { + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Checkbox state, true means checked. */ private boolean checked = false; - /** - * 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; - } - /** * Label for this checkbox. */ private String label; + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Public constructor. * @@ -77,7 +67,7 @@ public final class TCheckbox extends TWidget { * @param checked initial check state */ public TCheckbox(final TWidget parent, final int x, final int y, - final String label, final boolean checked) { + final String label, final boolean checked) { // Set parent and window super(parent, x, y, label.length() + 4, 1); @@ -89,6 +79,10 @@ public final class TCheckbox extends TWidget { setCursorX(1); } + // ------------------------------------------------------------------------ + // Event handlers --------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Returns true if the mouse is currently on the checkbox. * @@ -97,14 +91,47 @@ public final class TCheckbox extends TWidget { */ private boolean mouseOnCheckbox(final TMouseEvent mouse) { if ((mouse.getY() == 0) - && (mouse.getX() >= 0) - && (mouse.getX() <= 2) - ) { + && (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)) { + checked = !checked; + return; + } + + // Pass to parent for the things we don't care about. + super.onKeypress(keypress); + } + + // ------------------------------------------------------------------------ + // TWidget ---------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Draw a checkbox with label. */ @@ -128,33 +155,26 @@ public final class TCheckbox extends TWidget { getScreen().putStringXY(4, 0, label, checkboxColor); } + // ------------------------------------------------------------------------ + // TCheckbox -------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** - * Handle mouse checkbox presses. + * Get checked value. * - * @param mouse mouse button down event + * @return if true, this is checked */ - @Override - public void onMouseDown(final TMouseEvent mouse) { - if ((mouseOnCheckbox(mouse)) && (mouse.isMouse1())) { - // Switch state - checked = !checked; - } + public boolean isChecked() { + return checked; } /** - * Handle keystrokes. + * Set checked value. * - * @param keypress keystroke event + * @param checked new checked value. */ - @Override - public void onKeypress(final TKeypressEvent keypress) { - if (keypress.equals(kbSpace)) { - checked = !checked; - return; - } - - // Pass to parent for the things we don't care about. - super.onKeypress(keypress); + public void setChecked(final boolean checked) { + this.checked = checked; } } diff --git a/src/jexer/TCommand.java b/src/jexer/TCommand.java index a814fae..b6a0411 100644 --- a/src/jexer/TCommand.java +++ b/src/jexer/TCommand.java @@ -35,6 +35,10 @@ package jexer; */ public class TCommand { + // ------------------------------------------------------------------------ + // Constants -------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Immediately abort the application (e.g. remote side closed * connection). @@ -131,11 +135,40 @@ public class TCommand { */ public static final int SAVE = 30; + 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); + + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Type of command, one of EXIT, CASCADE, etc. */ private int type; + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Protected constructor. Subclasses can be used to define new commands. * @@ -145,6 +178,10 @@ public class TCommand { this.type = type; } + // ------------------------------------------------------------------------ + // TCommand --------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Make human-readable description of this TCommand. * @@ -181,25 +218,4 @@ public class TCommand { return type; } - 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); - } diff --git a/src/jexer/TEditColorThemeWindow.java b/src/jexer/TEditColorThemeWindow.java index 5b4abf3..bc3712b 100644 --- a/src/jexer/TEditColorThemeWindow.java +++ b/src/jexer/TEditColorThemeWindow.java @@ -51,6 +51,30 @@ public class TEditColorThemeWindow extends TWindow { */ 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. */ @@ -602,53 +626,9 @@ public class TEditColorThemeWindow extends TWindow { } - /** - * 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; - - /** - * 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); - } + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ /** * Public constructor. The file open box will be centered on screen. @@ -720,6 +700,31 @@ public class TEditColorThemeWindow extends TWindow { 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. */ @@ -747,21 +752,36 @@ public class TEditColorThemeWindow extends TWindow { i18n.getString("textTextText"), attr); } + // ------------------------------------------------------------------------ + // TEditColorThemeWindow -------------------------------------------------- + // ------------------------------------------------------------------------ + /** - * Handle keystrokes. + * Set various widgets/values to the editing theme color. * - * @param keypress keystroke event + * @param colorName name of color from theme */ - @Override - public void onKeypress(final TKeypressEvent keypress) { - // Escape - behave like cancel - if (keypress.equals(kbEsc)) { - getApplication().closeWindow(this); + 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; } - - // Pass to my parent - super.onKeypress(keypress); + 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/TField.java b/src/jexer/TField.java index c93cecb..8f693f2 100644 --- a/src/jexer/TField.java +++ b/src/jexer/TField.java @@ -39,31 +39,15 @@ import static jexer.TKeypress.*; */ public class TField extends TWidget { + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Field text. */ protected String text = ""; - /** - * Get field text. - * - * @return field text - */ - public final String getText() { - return text; - } - - /** - * Set field text. - * - * @param text the new field text - */ - public final void setText(String text) { - this.text = text; - position = 0; - windowStart = 0; - } - /** * If true, only allow enough characters that will fit in the width. If * false, allow the field to scroll to the right. @@ -100,6 +84,10 @@ public class TField extends TWidget { */ protected TAction updateAction; + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Public constructor. * @@ -157,6 +145,10 @@ public class TField extends TWidget { this.updateAction = updateAction; } + // ------------------------------------------------------------------------ + // Event handlers --------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Returns true if the mouse is currently on the field. * @@ -174,80 +166,6 @@ public class TField extends TWidget { return false; } - /** - * 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(); - } - } else { - if (updateAction != null) { - updateAction.DO(); - } - } - } - - /** - * Draw the text field. - */ - @Override - public void draw() { - CellAttributes fieldColor; - - if (isAbsoluteActive()) { - fieldColor = getTheme().getColor("tfield.active"); - } else { - fieldColor = getTheme().getColor("tfield.inactive"); - } - - int end = windowStart + getWidth(); - if (end > text.length()) { - end = text.length(); - } - getScreen().hLineXY(0, 0, getWidth(), GraphicsChars.HATCH, fieldColor); - getScreen().putStringXY(0, 0, text.substring(windowStart, end), - fieldColor); - - // Fix the cursor, it will be rendered by TApplication.drawAll(). - updateCursor(); - } - - /** - * Update the visible cursor position to match the location of position - * and windowStart. - */ - protected void updateCursor() { - if ((position > getWidth()) && fixed) { - setCursorX(getWidth()); - } else if ((position - windowStart == getWidth()) && !fixed) { - setCursorX(getWidth() - 1); - } else { - setCursorX(position - 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 = position - (getWidth() - 1); - if (windowStart < 0) { - windowStart = 0; - } - - updateCursor(); - } - /** * Handle mouse button presses. * @@ -419,6 +337,108 @@ public class TField extends TWidget { super.onKeypress(keypress); } + // ------------------------------------------------------------------------ + // TWidget ---------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Draw the text field. + */ + @Override + public void draw() { + CellAttributes fieldColor; + + if (isAbsoluteActive()) { + fieldColor = getTheme().getColor("tfield.active"); + } else { + fieldColor = getTheme().getColor("tfield.inactive"); + } + + int end = windowStart + getWidth(); + if (end > text.length()) { + end = text.length(); + } + getScreen().hLineXY(0, 0, getWidth(), GraphicsChars.HATCH, fieldColor); + getScreen().putStringXY(0, 0, text.substring(windowStart, end), + fieldColor); + + // Fix the cursor, it will be rendered by TApplication.drawAll(). + updateCursor(); + } + + // ------------------------------------------------------------------------ + // TField ----------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Get field text. + * + * @return field text + */ + public final String getText() { + return text; + } + + /** + * Set field text. + * + * @param text the new field text + */ + public final void setText(String text) { + 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(); + } + } else { + if (updateAction != null) { + updateAction.DO(); + } + } + } + + /** + * Update the visible cursor position to match the location of position + * and windowStart. + */ + protected void updateCursor() { + if ((position > getWidth()) && fixed) { + setCursorX(getWidth()); + } else if ((position - windowStart == getWidth()) && !fixed) { + setCursorX(getWidth() - 1); + } else { + setCursorX(position - 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 = position - (getWidth() - 1); + if (windowStart < 0) { + windowStart = 0; + } + + updateCursor(); + } + /** * Append char to the end of the field. * diff --git a/src/jexer/TFileOpenBox.java b/src/jexer/TFileOpenBox.java index 05629cf..18d65f2 100644 --- a/src/jexer/TFileOpenBox.java +++ b/src/jexer/TFileOpenBox.java @@ -34,6 +34,9 @@ import java.util.ResourceBundle; import jexer.bits.GraphicsChars; import jexer.event.TKeypressEvent; +import jexer.ttree.TDirectoryTreeItem; +import jexer.ttree.TTreeItem; +import jexer.ttree.TTreeViewWidget; import static jexer.TKeypress.*; /** @@ -59,6 +62,10 @@ public final class TFileOpenBox extends TWindow { */ private static final ResourceBundle i18n = ResourceBundle.getBundle(TFileOpenBox.class.getName()); + // ------------------------------------------------------------------------ + // Constants -------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * TFileOpenBox can be called for either Open or Save actions. */ @@ -74,24 +81,19 @@ public final class TFileOpenBox extends TWindow { SAVE } + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * String to return, or null if the user canceled. */ private String filename = null; - /** - * Get the return string. - * - * @return the filename the user selected, or null if they canceled. - */ - public String getFilename() { - return filename; - } - /** * The left-side tree view pane. */ - private TTreeView treeView; + private TTreeViewWidget treeView; /** * The data behind treeView. @@ -113,31 +115,9 @@ public final class TFileOpenBox extends TWindow { */ private TButton openButton; - /** - * 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()) { - filename = newFilename; - getApplication().closeWindow(this); - return; - } - if (newFile.isDirectory()) { - treeViewRoot = new TDirectoryTreeItem(treeView, newFilename, - true); - treeView.setTreeRoot(treeViewRoot, true); - treeView.reflowData(); - openButton.setEnabled(false); - directoryList.setPath(newFilename); - } - } - } + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ /** * Public constructor. The file open box will be centered on screen. @@ -168,7 +148,7 @@ public final class TFileOpenBox extends TWindow { entryField.onKeypress(new TKeypressEvent(kbEnd)); // Add directory treeView - treeView = addTreeView(1, 3, 30, getHeight() - 6, + treeView = addTreeViewWidget(1, 3, 30, getHeight() - 6, new TAction() { public void DO() { TTreeItem item = treeView.getSelected(); @@ -176,7 +156,7 @@ public final class TFileOpenBox extends TWindow { try { directoryList.setPath(selectedDir.getCanonicalPath()); openButton.setEnabled(false); - activate(directoryList); + activate(treeView); } catch (IOException e) { e.printStackTrace(); } @@ -250,15 +230,9 @@ public final class TFileOpenBox extends TWindow { getApplication().yield(); } - /** - * Draw me on screen. - */ - @Override - public void draw() { - super.draw(); - getScreen().vLineXY(33, 4, getHeight() - 6, GraphicsChars.WINDOW_SIDE, - getBackground()); - } + // ------------------------------------------------------------------------ + // Event handlers --------------------------------------------------------- + // ------------------------------------------------------------------------ /** * Handle keystrokes. @@ -275,8 +249,86 @@ public final class TFileOpenBox extends TWindow { 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()); + openButton.setEnabled(false); + activate(treeView); + } catch (IOException e) { + e.printStackTrace(); + } + return; + } + } + // Pass to my parent super.onKeypress(keypress); } + // ------------------------------------------------------------------------ + // TWidget ---------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Draw me on screen. + */ + @Override + public void draw() { + super.draw(); + getScreen().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()) { + filename = newFilename; + getApplication().closeWindow(this); + return; + } + if (newFile.isDirectory()) { + treeViewRoot = new TDirectoryTreeItem(treeView, + newFilename, true); + treeView.setTreeRoot(treeViewRoot, true); + openButton.setEnabled(false); + directoryList.setPath(newFilename); + } + } + } + } diff --git a/src/jexer/THScroller.java b/src/jexer/THScroller.java index 9e9b372..90133a4 100644 --- a/src/jexer/THScroller.java +++ b/src/jexer/THScroller.java @@ -37,11 +37,206 @@ import jexer.event.TMouseEvent; */ public final 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"); + getScreen().putCharXY(0, 0, GraphicsChars.CP437[0x11], arrowColor); + getScreen().putCharXY(getWidth() - 1, 0, GraphicsChars.CP437[0x10], + arrowColor); + + // Place the box + if (rightValue > leftValue) { + getScreen().hLineXY(1, 0, getWidth() - 2, GraphicsChars.CP437[0xB1], + barColor); + getScreen().putCharXY(boxPosition(), 0, GraphicsChars.BOX, + arrowColor); + } else { + getScreen().hLineXY(1, 0, getWidth() - 2, GraphicsChars.HATCH, + barColor); + } + + } + + // ------------------------------------------------------------------------ + // THScroller ------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Get the value that corresponds to being on the left edge of the scroll * bar. @@ -62,11 +257,6 @@ public final class THScroller extends TWidget { this.leftValue = leftValue; } - /** - * Value that corresponds to being on the right edge of the scroll bar. - */ - private int rightValue = 100; - /** * Get the value that corresponds to being on the right edge of the * scroll bar. @@ -87,11 +277,6 @@ public final class THScroller extends TWidget { this.rightValue = rightValue; } - /** - * Current value of the scroll. - */ - private int value = 0; - /** * Get current value of the scroll. * @@ -110,11 +295,6 @@ public final class THScroller extends TWidget { this.value = value; } - /** - * The increment for clicking on an arrow. - */ - private int smallChange = 1; - /** * Get the increment for clicking on an arrow. * @@ -133,11 +313,6 @@ public final class THScroller extends TWidget { this.smallChange = smallChange; } - /** - * The increment for clicking in the bar between the box and an arrow. - */ - private int bigChange = 20; - /** * Set the increment for clicking in the bar between the box and an * arrow. @@ -158,26 +333,6 @@ public final class THScroller extends TWidget { this.bigChange = bigChange; } - /** - * When true, the user is dragging the scroll box. - */ - private boolean inScroll = false; - - /** - * 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); - } - /** * Compute the position of the scroll box (a.k.a. grip, thumb). * @@ -187,30 +342,6 @@ public final class THScroller extends TWidget { return (getWidth() - 3) * (value - leftValue) / (rightValue - leftValue) + 1; } - /** - * Draw a horizontal scroll bar. - */ - @Override - public void draw() { - CellAttributes arrowColor = getTheme().getColor("tscroller.arrows"); - CellAttributes barColor = getTheme().getColor("tscroller.bar"); - getScreen().putCharXY(0, 0, GraphicsChars.CP437[0x11], arrowColor); - getScreen().putCharXY(getWidth() - 1, 0, GraphicsChars.CP437[0x10], - arrowColor); - - // Place the box - if (rightValue > leftValue) { - getScreen().hLineXY(1, 0, getWidth() - 2, GraphicsChars.CP437[0xB1], - barColor); - getScreen().putCharXY(boxPosition(), 0, GraphicsChars.BOX, - arrowColor); - } else { - getScreen().hLineXY(1, 0, getWidth() - 2, GraphicsChars.HATCH, - barColor); - } - - } - /** * Perform a small step change left. */ @@ -277,109 +408,4 @@ public final class THScroller extends TWidget { value = rightValue; } - /** - * 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; - 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; - } - - } - } diff --git a/src/jexer/TKeypress.java b/src/jexer/TKeypress.java index 872ebc4..2470bdf 100644 --- a/src/jexer/TKeypress.java +++ b/src/jexer/TKeypress.java @@ -33,6 +33,10 @@ package jexer; */ public final class TKeypress { + // ------------------------------------------------------------------------ + // Constants -------------------------------------------------------------- + // ------------------------------------------------------------------------ + // Various special keystrokes /** @@ -170,386 +174,10 @@ public final class TKeypress { */ public static final int ESC = 43; - /** - * If true, ch is meaningless, use keyCode instead. - */ - private boolean isFunctionKey; - - /** - * Getter for isFunctionKey. - * - * @return if true, ch is meaningless, use keyCode instead - */ - public boolean isFnKey() { - return isFunctionKey; - } - - /** - * Will be set to F1, F2, HOME, END, etc. if isKey is true. - */ - private int keyCode; - - /** - * Getter for function key code. - * - * @return function key code int value (only valid is isKey is true) - */ - public int getKeyCode() { - return keyCode; - } - - /** - * Keystroke modifier ALT. - */ - private boolean alt; - - /** - * Getter for ALT. - * - * @return alt value - */ - public boolean isAlt() { - return alt; - } - - /** - * Keystroke modifier CTRL. - */ - private boolean ctrl; - - /** - * Getter for CTRL. - * - * @return ctrl value - */ - public boolean isCtrl() { - return ctrl; - } - - /** - * Keystroke modifier SHIFT. - */ - private boolean shift; - - /** - * Getter for SHIFT. - * - * @return shift value - */ - public boolean isShift() { - return shift; - } - - /** - * The character received. - */ - private char ch; - - /** - * Getter for character. - * - * @return the character (only valid if isKey is false) - */ - public char getChar() { - return ch; - } - - /** - * 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 char 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; - } - - /** - * 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() { - 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; - } - - // Special "no-key" keypress, used to ignore undefined keystrokes - public static final TKeypress kbNoKey = new TKeypress(true, - TKeypress.NONE, ' ', false, false, false); + // 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, @@ -977,4 +605,393 @@ public final class TKeypress { 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 char 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 char 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 char 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() { + 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 index d5bb24c..2eeea91 100644 --- a/src/jexer/TLabel.java +++ b/src/jexer/TLabel.java @@ -35,34 +35,24 @@ import jexer.bits.CellAttributes; */ public final class TLabel extends TWidget { + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Label text. */ private String label = ""; - /** - * Get label text. - * - * @return label text - */ - public String getLabel() { - return label; - } - - /** - * Set label text. - * - * @param label new label text - */ - public void setLabel(final String label) { - this.label = label; - } - /** * Label color. */ private String colorKey; + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Public constructor, using the default "tlabel" for colorKey. * @@ -96,6 +86,10 @@ public final class TLabel extends TWidget { this.colorKey = colorKey; } + // ------------------------------------------------------------------------ + // TWidget ---------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Draw a static label. */ @@ -110,4 +104,26 @@ public final class TLabel extends TWidget { getScreen().putStringXY(0, 0, label, color); } + // ------------------------------------------------------------------------ + // TLabel ----------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Get label text. + * + * @return label text + */ + public String getLabel() { + return label; + } + + /** + * Set label text. + * + * @param label new label text + */ + public void setLabel(final String label) { + this.label = label; + } + } diff --git a/src/jexer/TList.java b/src/jexer/TList.java index e47d751..46c9307 100644 --- a/src/jexer/TList.java +++ b/src/jexer/TList.java @@ -41,6 +41,10 @@ import static jexer.TKeypress.*; */ public class TList extends TScrollableWidget { + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * The list of strings to display. */ @@ -51,56 +55,6 @@ public class TList extends TScrollableWidget { */ private int selectedString = -1; - /** - * 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 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; - } - - /** - * 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(); - } - /** * Maximum width of a single line. */ @@ -116,55 +70,9 @@ public class TList extends TScrollableWidget { */ private TAction moveAction = null; - /** - * Perform user selection action. - */ - public void dispatchEnter() { - assert (selectedString >= 0); - assert (selectedString < strings.size()); - if (enterAction != null) { - enterAction.DO(); - } - } - - /** - * Perform list movement action. - */ - public void dispatchMove() { - assert (selectedString >= 0); - assert (selectedString < strings.size()); - if (moveAction != null) { - moveAction.DO(); - } - } - - /** - * 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); - if (line.length() > maxLineWidth) { - maxLineWidth = line.length(); - } - } - - setBottomValue(strings.size() - getHeight() + 1); - if (getBottomValue() < 0) { - setBottomValue(0); - } - - setRightValue(maxLineWidth - getWidth() + 1); - if (getRightValue() < 0) { - setRightValue(0); - } - } + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ /** * Public constructor. @@ -241,48 +149,9 @@ public class TList extends TScrollableWidget { reflowData(); } - /** - * Draw the files 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) { - color = getTheme().getColor("tlist.selected"); - } else if (isAbsoluteActive()) { - color = getTheme().getColor("tlist"); - } else { - color = getTheme().getColor("tlist.inactive"); - } - String formatString = "%-" + Integer.toString(getWidth() - 1) + "s"; - getScreen().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++) { - getScreen().hLineXY(0, i, getWidth() - 1, ' ', color); - } - } + // ------------------------------------------------------------------------ + // Event handlers --------------------------------------------------------- + // ------------------------------------------------------------------------ /** * Handle mouse press events. @@ -427,4 +296,159 @@ public class TList extends TScrollableWidget { } } + // ------------------------------------------------------------------------ + // TScrollableWidget ------------------------------------------------------ + // ------------------------------------------------------------------------ + + /** + * 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); + if (line.length() > maxLineWidth) { + maxLineWidth = line.length(); + } + } + + setBottomValue(strings.size() - getHeight() + 1); + if (getBottomValue() < 0) { + setBottomValue(0); + } + + setRightValue(maxLineWidth - getWidth() + 1); + if (getRightValue() < 0) { + setRightValue(0); + } + } + + /** + * Draw the files 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"; + getScreen().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++) { + getScreen().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 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; + } + + /** + * 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(); + } + } + + /** + * Perform list movement action. + */ + public void dispatchMove() { + assert (selectedString >= 0); + assert (selectedString < strings.size()); + if (moveAction != null) { + moveAction.DO(); + } + } + } diff --git a/src/jexer/TProgressBar.java b/src/jexer/TProgressBar.java index 49fd76a..0677199 100644 --- a/src/jexer/TProgressBar.java +++ b/src/jexer/TProgressBar.java @@ -36,74 +36,28 @@ import jexer.bits.GraphicsChars; */ public final class TProgressBar extends TWidget { + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Value that corresponds to 0% progress. */ private int minValue = 0; - /** - * 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; - } - /** * Value that corresponds to 100% progress. */ private int maxValue = 100; - /** - * 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; - } - /** * Current value of the progress. */ private int value = 0; - /** - * 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; - } + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ /** * Public constructor. @@ -123,6 +77,15 @@ public final class TProgressBar extends TWidget { this.value = value; } + // ------------------------------------------------------------------------ + // Event handlers --------------------------------------------------------- + // ------------------------------------------------------------------------ + + + // ------------------------------------------------------------------------ + // TWidget ---------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Draw a static progress bar. */ @@ -158,4 +121,62 @@ public final class TProgressBar extends TWidget { 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; + } + } diff --git a/src/jexer/TRadioButton.java b/src/jexer/TRadioButton.java index 13a73ff..cdb56aa 100644 --- a/src/jexer/TRadioButton.java +++ b/src/jexer/TRadioButton.java @@ -39,32 +39,15 @@ import static jexer.TKeypress.*; */ public final class TRadioButton extends TWidget { + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * RadioButton state, true means selected. */ private boolean selected = false; - /** - * 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; - } - /** * Label for this radio button. */ @@ -76,15 +59,9 @@ public final class TRadioButton extends TWidget { */ private int id; - /** - * Get ID for this radio button. Buttons start counting at 1 in the - * RadioGroup. - * - * @return the ID - */ - public int getId() { - return id; - } + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ /** * Public constructor. @@ -108,6 +85,10 @@ public final class TRadioButton extends TWidget { setCursorX(1); } + // ------------------------------------------------------------------------ + // Event handlers --------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Returns true if the mouse is currently on the radio button. * @@ -124,30 +105,6 @@ public final class TRadioButton extends TWidget { return false; } - /** - * Draw a radio button with label. - */ - @Override - public void draw() { - CellAttributes radioButtonColor; - - if (isAbsoluteActive()) { - radioButtonColor = getTheme().getColor("tradiobutton.active"); - } else { - radioButtonColor = getTheme().getColor("tradiobutton.inactive"); - } - - getScreen().putCharXY(0, 0, '(', radioButtonColor); - if (selected) { - getScreen().putCharXY(1, 0, GraphicsChars.CP437[0x07], - radioButtonColor); - } else { - getScreen().putCharXY(1, 0, ' ', radioButtonColor); - } - getScreen().putCharXY(2, 0, ')', radioButtonColor); - getScreen().putStringXY(4, 0, label, radioButtonColor); - } - /** * Handle mouse button presses. * @@ -184,4 +141,67 @@ public final class TRadioButton extends TWidget { super.onKeypress(keypress); } + // ------------------------------------------------------------------------ + // TWidget ---------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Draw a radio button with label. + */ + @Override + public void draw() { + CellAttributes radioButtonColor; + + if (isAbsoluteActive()) { + radioButtonColor = getTheme().getColor("tradiobutton.active"); + } else { + radioButtonColor = getTheme().getColor("tradiobutton.inactive"); + } + + getScreen().putCharXY(0, 0, '(', radioButtonColor); + if (selected) { + getScreen().putCharXY(1, 0, GraphicsChars.CP437[0x07], + radioButtonColor); + } else { + getScreen().putCharXY(1, 0, ' ', radioButtonColor); + } + getScreen().putCharXY(2, 0, ')', radioButtonColor); + getScreen().putStringXY(4, 0, label, radioButtonColor); + } + + // ------------------------------------------------------------------------ + // 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; + } + } diff --git a/src/jexer/TStatusBar.java b/src/jexer/TStatusBar.java index 60b100a..975d285 100644 --- a/src/jexer/TStatusBar.java +++ b/src/jexer/TStatusBar.java @@ -29,6 +29,7 @@ package jexer; import java.util.ArrayList; +import java.util.List; import jexer.bits.CellAttributes; import jexer.bits.GraphicsChars; @@ -41,6 +42,25 @@ import jexer.event.TMouseEvent; */ public final 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. */ @@ -98,48 +118,9 @@ public final class TStatusBar extends TWidget { } - /** - * 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 ArrayList keys = new ArrayList(); - - /** - * 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; - } + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ /** * Public constructor. @@ -164,56 +145,9 @@ public final class TStatusBar extends TWidget { this(parent, ""); } - /** - * 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(); - - getScreen().hLineXY(0, row, width, ' ', barColor); - - int col = 0; - for (TStatusBarKey key: keys) { - String keyStr = key.key.toString(); - if (key.selected) { - getScreen().putCharXY(col++, row, ' ', selectedColor); - getScreen().putStringXY(col, row, keyStr, selectedColor); - col += keyStr.length(); - getScreen().putCharXY(col++, row, ' ', selectedColor); - getScreen().putStringXY(col, row, key.label, selectedColor); - col += key.label.length(); - getScreen().putCharXY(col++, row, ' ', selectedColor); - } else { - getScreen().putCharXY(col++, row, ' ', barColor); - getScreen().putStringXY(col, row, keyStr, keyColor); - col += keyStr.length() + 1; - getScreen().putStringXY(col, row, key.label, barColor); - col += key.label.length(); - getScreen().putCharXY(col++, row, ' ', barColor); - } - } - if (text.length() > 0) { - if (keys.size() > 0) { - getScreen().putCharXY(col++, row, GraphicsChars.VERTICAL_BAR, - barColor); - } - getScreen().putCharXY(col++, row, ' ', barColor); - getScreen().putStringXY(col, row, text, barColor); - } - } + // ------------------------------------------------------------------------ + // Event handlers --------------------------------------------------------- + // ------------------------------------------------------------------------ /** * Handle keypresses. @@ -302,4 +236,91 @@ public final class TStatusBar extends TWidget { } } + // ------------------------------------------------------------------------ + // 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(); + + getScreen().hLineXY(0, row, width, ' ', barColor); + + int col = 0; + for (TStatusBarKey key: keys) { + String keyStr = key.key.toString(); + if (key.selected) { + getScreen().putCharXY(col++, row, ' ', selectedColor); + getScreen().putStringXY(col, row, keyStr, selectedColor); + col += keyStr.length(); + getScreen().putCharXY(col++, row, ' ', selectedColor); + getScreen().putStringXY(col, row, key.label, selectedColor); + col += key.label.length(); + getScreen().putCharXY(col++, row, ' ', selectedColor); + } else { + getScreen().putCharXY(col++, row, ' ', barColor); + getScreen().putStringXY(col, row, keyStr, keyColor); + col += keyStr.length() + 1; + getScreen().putStringXY(col, row, key.label, barColor); + col += key.label.length(); + getScreen().putCharXY(col++, row, ' ', barColor); + } + } + if (text.length() > 0) { + if (keys.size() > 0) { + getScreen().putCharXY(col++, row, GraphicsChars.VERTICAL_BAR, + barColor); + } + getScreen().putCharXY(col++, row, ' ', barColor); + getScreen().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/TText.java b/src/jexer/TText.java index c57e884..d0148f2 100644 --- a/src/jexer/TText.java +++ b/src/jexer/TText.java @@ -28,21 +28,13 @@ */ package jexer; -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; - import java.util.LinkedList; import java.util.List; import jexer.bits.CellAttributes; import jexer.event.TKeypressEvent; import jexer.event.TMouseEvent; +import static jexer.TKeypress.*; /** * TText implements a simple scrollable text area. It reflows automatically on @@ -224,19 +216,19 @@ public final class TText extends TScrollableWidget { for (String p : paragraphs) { switch (justification) { case LEFT: - lines.addAll(jexer.bits.StringJustifier.left(p, + lines.addAll(jexer.bits.StringUtils.left(p, getWidth() - 1)); break; case CENTER: - lines.addAll(jexer.bits.StringJustifier.center(p, + lines.addAll(jexer.bits.StringUtils.center(p, getWidth() - 1)); break; case RIGHT: - lines.addAll(jexer.bits.StringJustifier.right(p, + lines.addAll(jexer.bits.StringUtils.right(p, getWidth() - 1)); break; case FULL: - lines.addAll(jexer.bits.StringJustifier.full(p, + lines.addAll(jexer.bits.StringUtils.full(p, getWidth() - 1)); break; } diff --git a/src/jexer/TTimer.java b/src/jexer/TTimer.java index a86c132..5d3dc13 100644 --- a/src/jexer/TTimer.java +++ b/src/jexer/TTimer.java @@ -35,6 +35,10 @@ import java.util.Date; */ public final class TTimer { + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * If true, re-schedule after every tick. Note package private access. */ @@ -50,6 +54,36 @@ public final class TTimer { */ 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. @@ -69,11 +103,6 @@ public final class TTimer { this.recurring = recurring; } - /** - * The action to perfom on a tick. - */ - private TAction action; - /** * Tick this timer. Note package private access. */ @@ -88,21 +117,4 @@ public final class TTimer { } } - /** - * 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); - } - } diff --git a/src/jexer/TVScroller.java b/src/jexer/TVScroller.java index 1817880..7a88739 100644 --- a/src/jexer/TVScroller.java +++ b/src/jexer/TVScroller.java @@ -37,11 +37,202 @@ import jexer.event.TMouseEvent; */ public final 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"); + getScreen().putCharXY(0, 0, GraphicsChars.CP437[0x1E], arrowColor); + getScreen().putCharXY(0, getHeight() - 1, GraphicsChars.CP437[0x1F], + arrowColor); + + // Place the box + if (bottomValue > topValue) { + getScreen().vLineXY(0, 1, getHeight() - 2, + GraphicsChars.CP437[0xB1], barColor); + getScreen().putCharXY(0, boxPosition(), GraphicsChars.BOX, + arrowColor); + } else { + getScreen().vLineXY(0, 1, getHeight() - 2, GraphicsChars.HATCH, + barColor); + } + + } + + // ------------------------------------------------------------------------ + // TVScroller ------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Get the value that corresponds to being on the top edge of the scroll * bar. @@ -62,11 +253,6 @@ public final class TVScroller extends TWidget { this.topValue = topValue; } - /** - * Value that corresponds to being on the bottom edge of the scroll bar. - */ - private int bottomValue = 100; - /** * Get the value that corresponds to being on the bottom edge of the * scroll bar. @@ -87,11 +273,6 @@ public final class TVScroller extends TWidget { this.bottomValue = bottomValue; } - /** - * Current value of the scroll. - */ - private int value = 0; - /** * Get current value of the scroll. * @@ -110,11 +291,6 @@ public final class TVScroller extends TWidget { this.value = value; } - /** - * The increment for clicking on an arrow. - */ - private int smallChange = 1; - /** * Get the increment for clicking on an arrow. * @@ -133,11 +309,6 @@ public final class TVScroller extends TWidget { this.smallChange = smallChange; } - /** - * The increment for clicking in the bar between the box and an arrow. - */ - private int bigChange = 20; - /** * Set the increment for clicking in the bar between the box and an * arrow. @@ -158,26 +329,6 @@ public final class TVScroller extends TWidget { this.bigChange = bigChange; } - /** - * When true, the user is dragging the scroll box. - */ - private boolean inScroll = false; - - /** - * 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); - } - /** * Compute the position of the scroll box (a.k.a. grip, thumb). * @@ -187,30 +338,6 @@ public final class TVScroller extends TWidget { return (getHeight() - 3) * (value - topValue) / (bottomValue - topValue) + 1; } - /** - * Draw a vertical scroll bar. - */ - @Override - public void draw() { - CellAttributes arrowColor = getTheme().getColor("tscroller.arrows"); - CellAttributes barColor = getTheme().getColor("tscroller.bar"); - getScreen().putCharXY(0, 0, GraphicsChars.CP437[0x1E], arrowColor); - getScreen().putCharXY(0, getHeight() - 1, GraphicsChars.CP437[0x1F], - arrowColor); - - // Place the box - if (bottomValue > topValue) { - getScreen().vLineXY(0, 1, getHeight() - 2, - GraphicsChars.CP437[0xB1], barColor); - getScreen().putCharXY(0, boxPosition(), GraphicsChars.BOX, - arrowColor); - } else { - getScreen().vLineXY(0, 1, getHeight() - 2, GraphicsChars.HATCH, - barColor); - } - - } - /** * Perform a small step change up. */ @@ -277,105 +404,4 @@ public final class TVScroller extends TWidget { value = bottomValue; } - /** - * 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; - 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; - } - } - } diff --git a/src/jexer/TWidget.java b/src/jexer/TWidget.java index 169e557..7d2ea35 100644 --- a/src/jexer/TWidget.java +++ b/src/jexer/TWidget.java @@ -41,6 +41,9 @@ import jexer.event.TMenuEvent; import jexer.event.TMouseEvent; import jexer.event.TResizeEvent; import jexer.menu.TMenu; +import jexer.ttree.TTreeItem; +import jexer.ttree.TTreeView; +import jexer.ttree.TTreeViewWidget; import static jexer.TKeypress.*; /** @@ -50,7 +53,7 @@ import static jexer.TKeypress.*; public abstract class TWidget implements Comparable { // ------------------------------------------------------------------------ - // Common widget attributes ----------------------------------------------- + // Variables -------------------------------------------------------------- // ------------------------------------------------------------------------ /** @@ -60,29 +63,11 @@ public abstract class TWidget implements Comparable { */ private TWidget parent = null; - /** - * Get parent widget. - * - * @return parent widget - */ - public final TWidget getParent() { - return parent; - } - /** * Child widgets that this widget contains. */ private List children; - /** - * Get the list of child widgets that this widget contains. - * - * @return the list of child widgets - */ - public List getChildren() { - return children; - } - /** * The currently active child widget that will receive keypress events. */ @@ -93,1024 +78,1058 @@ public abstract class TWidget implements Comparable { */ private boolean active = false; - /** - * 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; - } - /** * The window that this widget draws to. */ private TWindow window = null; - /** - * Get the window this widget is on. - * - * @return the window - */ - public final TWindow getWindow() { - return window; - } - /** * Absolute X position of the top-left corner. */ private int x = 0; /** - * Get X position. - * - * @return absolute X position of the top-left corner + * Absolute Y position of the top-left corner. */ - public final int getX() { - return x; - } + private int y = 0; /** - * Set X position. - * - * @param x absolute X position of the top-left corner + * Width. */ - public final void setX(final int x) { - this.x = x; - } + private int width = 0; /** - * Absolute Y position of the top-left corner. + * Height. */ - private int y = 0; + private int height = 0; /** - * Get Y position. - * - * @return absolute Y position of the top-left corner + * My tab order inside a window or containing widget. */ - public final int getY() { - return y; - } + private int tabOrder = 0; /** - * Set Y position. - * - * @param y absolute Y position of the top-left corner + * If true, this widget can be tabbed to or receive events. */ - public final void setY(final int y) { - this.y = y; - } + private boolean enabled = true; /** - * Width. + * If true, this widget has a cursor. */ - private int width = 0; + private boolean cursorVisible = false; /** - * Get the width. - * - * @return widget width + * Cursor column position in relative coordinates. */ - public final int getWidth() { - return this.width; - } + private int cursorX = 0; /** - * Change the width. - * - * @param width new widget width + * Cursor row position in relative coordinates. */ - public final void setWidth(final int width) { - this.width = width; - } + private int cursorY = 0; - /** - * Height. - */ - private int height = 0; + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ /** - * Get the height. - * - * @return widget height + * Default constructor for subclasses. */ - public final int getHeight() { - return this.height; + protected TWidget() { + children = new ArrayList(); } /** - * Change the height. + * Protected constructor. * - * @param height new widget height + * @param parent parent widget */ - public final void setHeight(final int height) { - this.height = height; + protected TWidget(final TWidget parent) { + this(parent, true); } /** - * Change the dimensions. + * Protected constructor. * - * @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 + * @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 final void setDimensions(final int x, final int y, final int width, - final int height) { + protected TWidget(final TWidget parent, final int x, final int y, + final int width, final int height) { - setX(x); - setY(y); - setWidth(width); - setHeight(height); + this(parent, true, x, y, width, height); } /** - * 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; - - /** - * Get enabled flag. + * Protected constructor used by subclasses that are disabled by default. * - * @return if true, this widget can be tabbed to or receive events + * @param parent parent widget + * @param enabled if true assume enabled */ - public final boolean isEnabled() { - return enabled; + protected TWidget(final TWidget parent, final boolean enabled) { + this.enabled = enabled; + this.parent = parent; + this.window = parent.window; + children = new ArrayList(); + + // Do not add TStatusBars, they are drawn by TApplication + if (this instanceof TStatusBar) { + } else { + parent.addChild(this); + } } /** - * Set enabled flag. + * Protected constructor used by subclasses that are disabled by default. * - * @param enabled if true, this widget can be tabbed to or receive events + * @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 */ - public final void setEnabled(final boolean enabled) { + protected TWidget(final TWidget parent, final boolean enabled, + final int x, final int y, final int width, final int height) { + 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; - } - } + this.parent = parent; + this.window = parent.window; + children = new ArrayList(); + + // Do not add TStatusBars, they are drawn by TApplication + if (this instanceof TStatusBar) { + } else { + parent.addChild(this); } - } - /** - * If true, this widget has a cursor. - */ - private boolean cursorVisible = false; + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } /** - * Set visible cursor flag. + * Backdoor access for TWindow's constructor. ONLY TWindow USES THIS. * - * @param cursorVisible if true, this widget has a cursor + * @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 */ - public final void setCursorVisible(final boolean cursorVisible) { - this.cursorVisible = cursorVisible; + protected final void setupForTWindow(final TWindow window, + final int x, final int y, final int width, final int height) { + + this.parent = window; + this.window = window; + this.x = x; + this.y = y; + this.width = width; + this.height = height; } + // ------------------------------------------------------------------------ + // Event handlers --------------------------------------------------------- + // ------------------------------------------------------------------------ + /** - * See if this widget has a visible cursor. + * Check if a mouse press/release event coordinate is contained in this + * widget. * - * @return if true, this widget has a visible cursor + * @param mouse a mouse-based event + * @return whether or not a mouse click would be sent to this widget */ - public final boolean isCursorVisible() { - // If cursor is out of my bounds, it is not visible. - if ((cursorX >= width) - || (cursorX < 0) - || (cursorY >= height) - || (cursorY < 0) - ) { + public final boolean mouseWouldHit(final TMouseEvent mouse) { + + if (!enabled) { return false; } - // 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) + if ((this instanceof TTreeItem) + && ((y < 0) || (y > parent.getHeight() - 1)) ) { return false; } - return cursorVisible; - } - /** - * Cursor column position in relative coordinates. - */ - private int cursorX = 0; + if ((mouse.getAbsoluteX() >= getAbsoluteX()) + && (mouse.getAbsoluteX() < getAbsoluteX() + width) + && (mouse.getAbsoluteY() >= getAbsoluteY()) + && (mouse.getAbsoluteY() < getAbsoluteY() + height) + ) { + return true; + } + return false; + } /** - * Get cursor X value. + * Method that subclasses can override to handle keystrokes. * - * @return cursor column position in relative coordinates + * @param keypress keystroke event */ - public final int getCursorX() { - return cursorX; + public void onKeypress(final TKeypressEvent keypress) { + + if ((children.size() == 0) + || (this instanceof TTreeView) + || (this instanceof TText) + ) { + + // 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)) + ) { + parent.switchWidget(true); + return; + } else if ((keypress.equals(kbShiftTab)) + || (keypress.equals(kbBackTab)) + || (keypress.equals(kbUp)) + ) { + 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; + } + } + } + + // Dispatch the keypress to an active widget + for (TWidget widget: children) { + if (widget.active) { + widget.onKeypress(keypress); + return; + } + } } /** - * Set cursor X value. + * Method that subclasses can override to handle mouse button presses. * - * @param cursorX column position in relative coordinates + * @param mouse mouse button event */ - public final void setCursorX(final int cursorX) { - this.cursorX = cursorX; - } + public void onMouseDown(final TMouseEvent mouse) { + // Default: do nothing, pass to children instead + 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); - /** - * Cursor row position in relative coordinates. - */ - private int cursorY = 0; + // 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; + } + } + } /** - * Get cursor Y value. + * Method that subclasses can override to handle mouse button releases. * - * @return cursor row position in relative coordinates + * @param mouse mouse button event */ - public final int getCursorY() { - return cursorY; + public void onMouseUp(final TMouseEvent mouse) { + // Default: do nothing, pass to children instead + 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; + } + } } /** - * Set cursor Y value. + * Method that subclasses can override to handle mouse movements. * - * @param cursorY row position in relative coordinates + * @param mouse mouse motion event */ - public final void setCursorY(final int cursorY) { - this.cursorY = cursorY; + 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); + } } - // ------------------------------------------------------------------------ - // TApplication integration ----------------------------------------------- - // ------------------------------------------------------------------------ - /** - * Get this TWidget's parent TApplication. + * Method that subclasses can override to handle mouse button + * double-clicks. * - * @return the parent TApplication + * @param mouse mouse button event */ - public TApplication getApplication() { - return window.getApplication(); + public void onMouseDoubleClick(final TMouseEvent mouse) { + // Default: do nothing, pass to children instead + 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; + } + } } /** - * Get the Screen. + * Method that subclasses can override to handle window/screen resize + * events. * - * @return the Screen + * @param resize resize event */ - public Screen getScreen() { - return window.getScreen(); + public void onResize(final TResizeEvent resize) { + // Default: change my width/height. + if (resize.getType() == TResizeEvent.Type.WIDGET) { + width = resize.getWidth(); + height = resize.getHeight(); + } else { + // Let children see the screen resize + for (TWidget widget: children) { + widget.onResize(resize); + } + } } /** - * Comparison operator. For various subclasses it sorts on: - *
    - *
  • tabOrder for TWidgets
  • - *
  • z for TWindows
  • - *
  • text for TTreeItems
  • - *
+ * Method that subclasses can override to handle posted command events. * - * @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) + * @param command command event */ - 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())); + public void onCommand(final TCommandEvent command) { + // Default: do nothing, pass to children instead + for (TWidget widget: children) { + widget.onCommand(command); } - return (this.tabOrder - that.tabOrder); } /** - * See if this widget should render with the active color. + * Method that subclasses can override to handle menu or posted menu + * events. * - * @return true if this widget is active and all of its parents are - * active. + * @param menu menu event */ - public final boolean isAbsoluteActive() { - if (parent == this) { - return active; + public void onMenu(final TMenuEvent menu) { + // Default: do nothing, pass to children instead + for (TWidget widget: children) { + widget.onMenu(menu); } - return (active && parent.isAbsoluteActive()); } /** - * Returns the cursor X position. - * - * @return absolute screen column number for the cursor's X position + * 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 final int getCursorAbsoluteX() { - return getAbsoluteX() + cursorX; + public void onIdle() { + // Default: do nothing, pass to children instead + for (TWidget widget: children) { + widget.onIdle(); + } } /** - * Returns the cursor Y position. + * Consume event. Subclasses that want to intercept all events in one go + * can override this method. * - * @return absolute screen row number for the cursor's Y position + * @param event keyboard, mouse, resize, command, or menu event */ - public final int getCursorAbsoluteY() { - return getAbsoluteY() + cursorY; + 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 ---------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** - * Compute my absolute X position as the sum of my X plus all my parent's - * X's. + * Get parent widget. * - * @return absolute screen column number for my X position + * @return parent widget */ - 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; + public final TWidget getParent() { + return parent; } /** - * Compute my absolute Y position as the sum of my Y plus all my parent's - * Y's. + * Get the list of child widgets that this widget contains. * - * @return absolute screen row number for my Y position + * @return the list of child widgets */ - 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; + public List getChildren() { + return children; } /** - * Get the global color theme. + * Get active flag. * - * @return the ColorTheme + * @return if true, this widget will receive events */ - public final ColorTheme getTheme() { - return window.getApplication().getTheme(); + public final boolean isActive() { + return active; } /** - * Draw my specific widget. When called, the screen rectangle I draw - * into is already setup (offset and clipping). + * Set active flag. + * + * @param active if true, this widget will receive events */ - public void draw() { - // Default widget draws nothing. + public final void setActive(final boolean active) { + this.active = active; } /** - * Called by parent to render to TWindow. + * Get the window this widget is on. + * + * @return the window */ - public final void drawChildren() { - // 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)) { - absoluteRightEdge -= 1; - } - if (!(this instanceof TWindow) && !(this instanceof THScroller)) { - 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(); - - // Continue down the chain - for (TWidget widget: children) { - widget.drawChildren(); - } + public final TWindow getWindow() { + return window; } /** - * Repaint the screen on the next update. + * Get X position. + * + * @return absolute X position of the top-left corner */ - public void doRepaint() { - window.getApplication().doRepaint(); + public final int getX() { + return x; } - // ------------------------------------------------------------------------ - // Constructors ----------------------------------------------------------- - // ------------------------------------------------------------------------ - /** - * Default constructor for subclasses. + * Set X position. + * + * @param x absolute X position of the top-left corner */ - protected TWidget() { - children = new ArrayList(); + public final void setX(final int x) { + this.x = x; } /** - * Protected constructor. + * Get Y position. * - * @param parent parent widget + * @return absolute Y position of the top-left corner */ - protected TWidget(final TWidget parent) { - this(parent, true); + public final int getY() { + return y; } /** - * Protected constructor. + * Set Y position. * - * @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 y absolute Y position of the top-left corner */ - protected TWidget(final TWidget parent, final int x, final int y, - final int width, final int height) { - - this(parent, true, x, y, width, height); + public final void setY(final int y) { + this.y = y; } /** - * Protected constructor used by subclasses that are disabled by default. + * Get the width. * - * @param parent parent widget - * @param enabled if true assume enabled + * @return widget width */ - protected TWidget(final TWidget parent, final boolean enabled) { - this.enabled = enabled; - this.parent = parent; - this.window = parent.window; - children = new ArrayList(); - - // Do not add TStatusBars, they are drawn by TApplication - if (this instanceof TStatusBar) { - } else { - parent.addChild(this); - } + public final int getWidth() { + return this.width; } /** - * Protected constructor used by subclasses that are disabled by default. + * Change the width. * - * @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 + * @param width new widget width */ - protected TWidget(final TWidget parent, final boolean enabled, - final int x, final int y, final int width, final int height) { - - this.enabled = enabled; - this.parent = parent; - this.window = parent.window; - children = new ArrayList(); - - // Do not add TStatusBars, they are drawn by TApplication - if (this instanceof TStatusBar) { - } else { - parent.addChild(this); - } - - this.x = x; - this.y = y; + public final void setWidth(final int width) { this.width = width; - this.height = height; } /** - * Backdoor access for TWindow's constructor. ONLY TWindow USES THIS. + * Get the height. * - * @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 + * @return widget height */ - protected final void setupForTWindow(final TWindow window, - final int x, final int y, final int width, final int height) { + public final int getHeight() { + return this.height; + } - this.parent = window; - this.window = window; - this.x = x; - this.y = y; - this.width = width; + /** + * Change the height. + * + * @param height new widget height + */ + public final void setHeight(final int height) { this.height = height; } - // ------------------------------------------------------------------------ - // General behavior ------------------------------------------------------- - // ------------------------------------------------------------------------ - /** - * Add a child widget to my list of children. We set its tabOrder to 0 - * and increment the tabOrder of all other children. + * Change the dimensions. * - * @param child TWidget to add + * @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 */ - private void addChild(final TWidget child) { - children.add(child); + public final void setDimensions(final int x, final int y, final int width, + final int height) { - 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; - } + setX(x); + setY(y); + setWidth(width); + setHeight(height); } /** - * Switch the active child. + * Get enabled flag. * - * @param child TWidget to activate + * @return if true, this widget can be tabbed to or receive events */ - public final void activate(final TWidget child) { - assert (child.enabled); - if ((child instanceof THScroller) - || (child instanceof TVScroller) - ) { - return; - } + public final boolean isEnabled() { + return enabled; + } - if (child != activeChild) { - if (activeChild != null) { - activeChild.active = false; + /** + * 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; + } } - child.active = true; - activeChild = child; } } /** - * Switch the active child. + * Set visible cursor flag. * - * @param tabOrder tabOrder of the child to activate. If that child - * isn't enabled, then the next enabled child will be activated. + * @param cursorVisible if true, this widget has a cursor */ - public final void activate(final int tabOrder) { - if (activeChild == null) { - 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)) { - activeChild.active = false; - assert (child.enabled); - child.active = true; - activeChild = child; - } + public final void setCursorVisible(final boolean cursorVisible) { + this.cursorVisible = cursorVisible; } /** - * Switch the active widget with the next in the tab order. + * See if this widget has a visible cursor. * - * @param forward if true, then switch to the next enabled widget in the - * list, otherwise switch to the previous enabled widget in the list + * @return if true, this widget has a visible cursor */ - public final void switchWidget(final boolean forward) { + 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; + } - // Only switch if there are multiple enabled widgets - if ((children.size() < 2) || (activeChild == null)) { - return; + // 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; + } - int tabOrder = activeChild.tabOrder; - do { - if (forward) { - tabOrder++; - } else { - tabOrder--; - } - if (tabOrder < 0) { + /** + * Get cursor X value. + * + * @return cursor column position in relative coordinates + */ + public final int getCursorX() { + return cursorX; + } - // If at the end, pass the switch to my parent. - if ((!forward) && (parent != this)) { - parent.switchWidget(forward); - return; - } + /** + * Set cursor X value. + * + * @param cursorX column position in relative coordinates + */ + public final void setCursorX(final int cursorX) { + this.cursorX = cursorX; + } - 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; - } + /** + * Get cursor Y value. + * + * @return cursor row position in relative coordinates + */ + public final int getCursorY() { + return cursorY; + } - tabOrder = 0; - } - if (activeChild.tabOrder == tabOrder) { - // We wrapped around - break; - } - } while ((!children.get(tabOrder).enabled) - && !(children.get(tabOrder) instanceof THScroller) - && !(children.get(tabOrder) instanceof TVScroller)); + /** + * Set cursor Y value. + * + * @param cursorY row position in relative coordinates + */ + public final void setCursorY(final int cursorY) { + this.cursorY = cursorY; + } - assert (children.get(tabOrder).enabled); + /** + * Get this TWidget's parent TApplication. + * + * @return the parent TApplication + */ + public TApplication getApplication() { + return window.getApplication(); + } - activeChild.active = false; - children.get(tabOrder).active = true; - activeChild = children.get(tabOrder); + /** + * Get the Screen. + * + * @return the Screen + */ + public Screen getScreen() { + return window.getScreen(); } /** - * Returns my active widget. + * Comparison operator. For various subclasses it sorts on: + *
    + *
  • tabOrder for TWidgets
  • + *
  • z for TWindows
  • + *
  • text for TTreeItems
  • + *
* - * @return widget that is active, or this if no children + * @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) */ - public TWidget getActiveChild() { - if ((this instanceof THScroller) - || (this instanceof TVScroller) + public final int compareTo(final TWidget that) { + if ((this instanceof TWindow) + && (that instanceof TWindow) ) { - return parent; + 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); + } - for (TWidget widget: children) { - if (widget.active) { - return widget.getActiveChild(); - } + /** + * 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; } - // No active children, return me - return this; + return (active && parent.isAbsoluteActive()); } - // ------------------------------------------------------------------------ - // Event handlers --------------------------------------------------------- - // ------------------------------------------------------------------------ + /** + * Returns the cursor X position. + * + * @return absolute screen column number for the cursor's X position + */ + public final int getCursorAbsoluteX() { + return getAbsoluteX() + cursorX; + } /** - * Check if a mouse press/release event coordinate is contained in this - * widget. + * Returns the cursor Y position. * - * @param mouse a mouse-based event - * @return whether or not a mouse click would be sent to this widget + * @return absolute screen row number for the cursor's Y position */ - public final boolean mouseWouldHit(final TMouseEvent mouse) { + public final int getCursorAbsoluteY() { + return getAbsoluteY() + cursorY; + } - if (!enabled) { - return false; + /** + * 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; + } - if ((mouse.getAbsoluteX() >= getAbsoluteX()) - && (mouse.getAbsoluteX() < getAbsoluteX() + width) - && (mouse.getAbsoluteY() >= getAbsoluteY()) - && (mouse.getAbsoluteY() < getAbsoluteY() + height) + /** + * 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) ) { - return true; + // 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 false; + return parent.getAbsoluteY() + y; } /** - * Method that subclasses can override to handle keystrokes. + * Get the global color theme. * - * @param keypress keystroke event + * @return the ColorTheme */ - public void onKeypress(final TKeypressEvent keypress) { + public final ColorTheme getTheme() { + return window.getApplication().getTheme(); + } - if ((children.size() == 0) - || (this instanceof TTreeView) - || (this instanceof TText) - ) { + /** + * 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. + } - // Defaults: - // tab / shift-tab - switch to next/previous widget - // right-arrow or down-arrow: same as tab - // left-arrow or up-arrow: same as shift-tab - if ((keypress.equals(kbTab)) - || (keypress.equals(kbRight)) - || (keypress.equals(kbDown)) - ) { - parent.switchWidget(true); - return; - } else if ((keypress.equals(kbShiftTab)) - || (keypress.equals(kbBackTab)) - || (keypress.equals(kbLeft)) - || (keypress.equals(kbUp)) - ) { - parent.switchWidget(false); - return; - } + /** + * Called by parent to render to TWindow. + */ + public final void drawChildren() { + // 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; } - // 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())) - ) { + screen.setClipRight(width); + screen.setClipBottom(height); - widget.handleEvent(new TKeypressEvent(kbEnter)); - return; - } - } + int absoluteRightEdge = window.getAbsoluteX() + window.getWidth(); + int absoluteBottomEdge = window.getAbsoluteY() + window.getHeight(); + if (!(this instanceof TWindow) && !(this instanceof TVScroller)) { + absoluteRightEdge -= 1; + } + if (!(this instanceof TWindow) && !(this instanceof THScroller)) { + 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()); - // Dispatch the keypress to an active widget + // Draw me + draw(); + + // Continue down the chain for (TWidget widget: children) { - if (widget.active) { - widget.handleEvent(keypress); - return; - } + widget.drawChildren(); } } /** - * Method that subclasses can override to handle mouse button presses. - * - * @param mouse mouse button event + * Repaint the screen on the next update. */ - public void onMouseDown(final TMouseEvent mouse) { - // Default: do nothing, pass to children instead - 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.handleEvent(mouse); - return; - } - } + public void doRepaint() { + window.getApplication().doRepaint(); } /** - * Method that subclasses can override to handle mouse button releases. + * Add a child widget to my list of children. We set its tabOrder to 0 + * and increment the tabOrder of all other children. * - * @param mouse mouse button event + * @param child TWidget to add */ - public void onMouseUp(final TMouseEvent mouse) { - // Default: do nothing, pass to children instead - 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); + private void addChild(final TWidget child) { + children.add(child); - // 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; + if ((child.enabled) + && !(child instanceof THScroller) + && !(child instanceof TVScroller) + ) { + for (TWidget widget: children) { + widget.active = false; } + child.active = true; + activeChild = child; } - } - - /** - * 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.handleEvent(mouse); + for (int i = 0; i < children.size(); i++) { + children.get(i).tabOrder = i; } } /** - * Method that subclasses can override to handle mouse button - * double-clicks. + * Switch the active child. * - * @param mouse mouse button event + * @param child TWidget to activate */ - public void onMouseDoubleClick(final TMouseEvent mouse) { - // Default: do nothing, pass to children instead - 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.handleEvent(mouse); - return; - } + public final void activate(final TWidget child) { + assert (child.enabled); + if ((child instanceof THScroller) + || (child instanceof TVScroller) + ) { + 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(); - } else { - // Let children see the screen resize - for (TWidget widget: children) { - widget.onResize(resize); + if (child != activeChild) { + if (activeChild != null) { + activeChild.active = false; } + child.active = true; + activeChild = child; } } /** - * Method that subclasses can override to handle posted command events. + * Switch the active child. * - * @param command command event + * @param tabOrder tabOrder of the child to activate. If that child + * isn't enabled, then the next enabled child will be activated. */ - public void onCommand(final TCommandEvent command) { - // Default: do nothing, pass to children instead - for (TWidget widget: children) { - widget.onCommand(command); + public final void activate(final int tabOrder) { + if (activeChild == null) { + return; } - } - - /** - * 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 + TWidget child = null; for (TWidget widget: children) { - widget.onMenu(menu); + if ((widget.enabled) + && !(widget instanceof THScroller) + && !(widget instanceof TVScroller) + && (widget.tabOrder >= tabOrder) + ) { + child = widget; + break; + } } - } - - /** - * 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(); + if ((child != null) && (child != activeChild)) { + activeChild.active = false; + assert (child.enabled); + child.active = true; + activeChild = child; } } /** - * Consume event. Subclasses that want to intercept all events in one go - * can override this method. + * Switch the active widget with the next in the tab order. * - * @param event keyboard, mouse, resize, command, or menu event + * @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 void handleEvent(final TInputEvent event) { - // System.err.printf("TWidget (%s) event: %s\n", this.getClass().getName(), - // event); + public final void switchWidget(final boolean forward) { - if (!enabled) { - // Discard event - // System.err.println(" -- discard --"); + // Only switch if there are multiple enabled widgets + if ((children.size() < 2) || (activeChild == null)) { return; } - if (event instanceof TKeypressEvent) { - onKeypress((TKeypressEvent) event); - } else if (event instanceof TMouseEvent) { + int tabOrder = activeChild.tabOrder; + do { + if (forward) { + tabOrder++; + } else { + tabOrder--; + } + if (tabOrder < 0) { - TMouseEvent mouse = (TMouseEvent) event; + // If at the end, pass the switch to my parent. + if ((!forward) && (parent != this)) { + parent.switchWidget(forward); + return; + } - switch (mouse.getType()) { + 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; + } - case MOUSE_DOWN: - onMouseDown(mouse); + tabOrder = 0; + } + if (activeChild.tabOrder == tabOrder) { + // We wrapped around break; + } + } while ((!children.get(tabOrder).enabled) + && !(children.get(tabOrder) instanceof THScroller) + && !(children.get(tabOrder) instanceof TVScroller)); - case MOUSE_UP: - onMouseUp(mouse); - break; + assert (children.get(tabOrder).enabled); - case MOUSE_MOTION: - onMouseMotion(mouse); - break; + activeChild.active = false; + children.get(tabOrder).active = true; + activeChild = children.get(tabOrder); + } - case MOUSE_DOUBLE_CLICK: - onMouseDoubleClick(mouse); - break; + /** + * 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; + } - default: - throw new IllegalArgumentException("Invalid mouse event type: " - + mouse.getType()); + for (TWidget widget: children) { + if (widget.active) { + return widget.getActiveChild(); } - } 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; + // No active children, return me + return this; } // ------------------------------------------------------------------------ @@ -1421,7 +1440,8 @@ public abstract class TWidget implements Comparable { } /** - * Convenience function to add a tree view to this container/window. + * Convenience function to add a scrollable tree view to this + * container/window. * * @param x column relative to parent * @param y row relative to parent @@ -1429,14 +1449,15 @@ public abstract class TWidget implements Comparable { * @param height height of tree view * @return the new tree view */ - public final TTreeView addTreeView(final int x, final int y, + public final TTreeViewWidget addTreeViewWidget(final int x, final int y, final int width, final int height) { - return new TTreeView(this, x, y, width, height); + return new TTreeViewWidget(this, x, y, width, height); } /** - * Convenience function to add a tree view to this container/window. + * Convenience function to add a scrollable tree view to this + * container/window. * * @param x column relative to parent * @param y row relative to parent @@ -1445,10 +1466,10 @@ public abstract class TWidget implements Comparable { * @param action action to perform when an item is selected * @return the new tree view */ - public final TTreeView addTreeView(final int x, final int y, + public final TTreeViewWidget addTreeViewWidget(final int x, final int y, final int width, final int height, final TAction action) { - return new TTreeView(this, x, y, width, height, action); + return new TTreeViewWidget(this, x, y, width, height, action); } /** diff --git a/src/jexer/TWindow.java b/src/jexer/TWindow.java index 19c96fd..140a38a 100644 --- a/src/jexer/TWindow.java +++ b/src/jexer/TWindow.java @@ -29,6 +29,7 @@ package jexer; import java.util.HashSet; +import java.util.Set; import jexer.backend.Screen; import jexer.bits.Cell; @@ -49,7 +50,7 @@ import static jexer.TKeypress.*; public class TWindow extends TWidget { // ------------------------------------------------------------------------ - // Public constants ------------------------------------------------------- + // Constants -------------------------------------------------------------- // ------------------------------------------------------------------------ /** @@ -78,162 +79,48 @@ public class TWindow extends TWidget { */ public static final int NOZOOMBOX = 0x10; - // ------------------------------------------------------------------------ - // Common window attributes ----------------------------------------------- - // ------------------------------------------------------------------------ - - /** - * Window flags. Note package private access. - */ - int flags = RESIZABLE; - /** - * Window title. - */ - private String title = ""; - - /** - * Get window title. - * - * @return window title + * Window is placed at absolute position (no smart placement) (default + * no). */ - public final String getTitle() { - return title; - } + public static final int ABSOLUTEXY = 0x20; /** - * Set window title. - * - * @param title new window title + * Hitting the closebox with the mouse calls TApplication.hideWindow() + * rather than TApplication.closeWindow() (default no). */ - public final void setTitle(final String title) { - this.title = title; - } + public static final int HIDEONCLOSE = 0x40; // ------------------------------------------------------------------------ - // TApplication integration ----------------------------------------------- + // Variables -------------------------------------------------------------- // ------------------------------------------------------------------------ /** - * Window's parent TApplication. + * Window flags. Note package private access. */ - private TApplication application; + int flags = RESIZABLE; /** - * Get this TWindow's parent TApplication. - * - * @return this TWindow's parent TApplication + * Window title. */ - @Override - public final TApplication getApplication() { - return application; - } + private String title = ""; /** - * Get the Screen. - * - * @return the Screen + * Window's parent TApplication. */ - @Override - public final Screen getScreen() { - return application.getScreen(); - } + private TApplication application; /** * Z order. Lower number means more in-front. */ private int z = 0; - /** - * 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; - } - /** * Window's keyboard shortcuts. Any key in this set will be passed to * the window directly rather than processed through the menu * accelerators. */ - private HashSet keyboardShortcuts = new HashSet(); - - /** - * 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); - } - - /** - * 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; - - /** - * 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; - } - - // ------------------------------------------------------------------------ - // Window movement/resizing support --------------------------------------- - // ------------------------------------------------------------------------ + private Set keyboardShortcuts = new HashSet(); /** * If true, then the user clicked on the title bar and is moving the @@ -251,7 +138,7 @@ public class TWindow extends TWidget { * If true, then the user selected "Size/Move" (or hit Ctrl-F5) and is * resizing/moving the window via the keyboard. */ - private boolean inKeyboardResize = false; + protected boolean inKeyboardResize = false; /** * If true, this window is maximized. @@ -284,226 +171,67 @@ public class TWindow extends TWidget { private int restoreWindowY; /** - * Set the maximum width for this window. - * - * @param maximumWindowWidth new maximum width + * 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. */ - 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; - } + boolean hidden = false; /** - * Set the minimum width for this window. - * - * @param minimumWindowWidth new minimum width + * 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. */ - 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; - } + protected TStatusBar statusBar = null; + + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ /** - * Set the maximum height for this window. + * Public constructor. Window will be located at (0, 0). * - * @param maximumWindowHeight new maximum height + * @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 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; + public TWindow(final TApplication application, final String title, + final int width, final int height) { + + this(application, title, 0, 0, width, height, RESIZABLE); } /** - * Set the minimum height for this window. + * Public constructor. Window will be located at (0, 0). * - * @param minimumWindowHeight new minimum height + * @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 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; - } + public TWindow(final TApplication application, final String title, + final int width, final int height, final int flags) { - /** - * 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()); - } + this(application, title, 0, 0, width, height, flags); } /** - * Maximize window. + * 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 void maximize() { - if (maximized) { - return; - } + public TWindow(final TApplication application, final String title, + final int x, final int y, final int width, final int height) { - restoreWindowWidth = getWidth(); - restoreWindowHeight = getHeight(); - restoreWindowX = getX(); - restoreWindowY = getY(); - setWidth(getScreen().getWidth()); - setHeight(application.getDesktopBottom() - 1); - setX(0); - setY(1); - maximized = true; - } - - /** - * Restore (unmaximize) window. - */ - public void restore() { - if (!maximized) { - return; - } - - setWidth(restoreWindowWidth); - setHeight(restoreWindowHeight); - setX(restoreWindowX); - setY(restoreWindowY); - maximized = false; - } - - // ------------------------------------------------------------------------ - // Window visibility ------------------------------------------------------ - // ------------------------------------------------------------------------ - - /** - * 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; - - /** - * 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). - */ - public void activate() { - application.activateWindow(this); - } - - /** - * Close window. Note that windows without a close box can still be - * closed by calling the close() method. - */ - public void close() { - application.closeWindow(this); - } - - // ------------------------------------------------------------------------ - // 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); + this(application, title, x, y, width, height, RESIZABLE); } /** @@ -545,310 +273,96 @@ public class TWindow extends TWidget { center(); // Add me to the application - application.addWindow(this); + application.addWindowToApplication(this); } // ------------------------------------------------------------------------ - // General behavior ------------------------------------------------------- + // Event handlers --------------------------------------------------------- // ------------------------------------------------------------------------ /** - * See if this window is undergoing any movement/resize/etc. + * Returns true if the mouse is currently on the close button. * - * @return true if the window is moving + * @return true if mouse is currently on the close button */ - public boolean inMovements() { - if (inWindowResize || inWindowMove || inKeyboardResize) { + protected boolean mouseOnClose() { + if ((flags & NOCLOSEBOX) != 0) { + return false; + } + if ((mouse != null) + && (mouse.getAbsoluteY() == getY()) + && (mouse.getAbsoluteX() == getX() + 3) + ) { 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. + * Returns true if the mouse is currently on the maximize/restore button. * - * @return true if this window is modal + * @return true if the mouse is currently on the maximize/restore button */ - public final boolean isModal() { - if ((flags & MODAL) == 0) { + protected boolean mouseOnMaximize() { + if ((flags & NOZOOMBOX) != 0) { return false; } - return true; + if ((mouse != null) + && !isModal() + && (mouse.getAbsoluteY() == getY()) + && (mouse.getAbsoluteX() == getX() + getWidth() - 4) + ) { + return true; + } + return false; } /** - * Returns true if this window has a close box. + * Returns true if the mouse is currently on the resizable lower right + * corner. * - * @return true if this window has a close box + * @return true if the mouse is currently on the resizable lower right + * corner */ - public final boolean hasCloseBox() { - if ((flags & NOCLOSEBOX) != 0) { + 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; } /** - * Returns true if this window has a maximize/zoom box. - * - * @return true if this window has a maximize/zoom box + * Subclasses should override this method to cleanup resources. This is + * called by application.closeWindow(). */ - public final boolean hasZoomBox() { - if ((flags & NOZOOMBOX) != 0) { - return true; - } - return false; + public void onClose() { + // Default: do nothing } /** - * Retrieve the background color. - * - * @return the background color + * Called by application.switchWindow() when this window gets the + * focus, and also by application.addWindow(). */ - 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"); - } + public void onFocus() { + // Default: do nothing } /** - * Retrieve the border color. - * - * @return the border color + * Called by application.switchWindow() when another window gets the + * focus. */ - public CellAttributes getBorder() { - if (!isModal() - && (inWindowMove || inWindowResize || inKeyboardResize) - ) { - assert (isActive()); - 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"); - } + public void onUnfocus() { + // Default: do nothing } /** - * 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; - } - } - - /** - * 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(); - - getScreen().drawBox(0, 0, getWidth(), getHeight(), border, - background, borderType, true); - - // Draw the title - int titleLeft = (getWidth() - title.length() - 2) / 2; - putCharXY(titleLeft, 0, ' ', border); - putStringXY(titleLeft + 1, 0, title); - putCharXY(titleLeft + title.length() + 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], - !isModal() - ? getTheme().getColor("twindow.border.windowmove") - : getTheme().getColor("twindow.border.modal.windowmove")); - } else { - putCharXY(3, 0, GraphicsChars.CP437[0xFE], - !isModal() - ? getTheme().getColor("twindow.border.windowmove") - : getTheme().getColor("twindow.border.modal.windowmove")); - } - } - - // 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], - getTheme().getColor("twindow.border.windowmove")); - } else { - if (maximized) { - putCharXY(getWidth() - 4, 0, GraphicsChars.CP437[0x12], - getTheme().getColor("twindow.border.windowmove")); - } else { - putCharXY(getWidth() - 4, 0, GraphicsChars.UPARROW, - getTheme().getColor("twindow.border.windowmove")); - } - } - - // Draw the resize corner - if ((flags & RESIZABLE) != 0) { - putCharXY(getWidth() - 2, getHeight() - 1, - GraphicsChars.SINGLE_BAR, - getTheme().getColor("twindow.border.windowmove")); - putCharXY(getWidth() - 1, getHeight() - 1, - GraphicsChars.LRCORNER, - getTheme().getColor("twindow.border.windowmove")); - } - } - } - } - - // ------------------------------------------------------------------------ - // 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 cleanup resources. This is - * called by application.closeWindow(). - */ - public void onClose() { - // Default: do nothing - } - - /** - * Called by application.switchWindow() when this window gets the - * focus, and also by application.addWindow(). - */ - public void onFocus() { - // Default: do nothing - } - - /** - * Called by application.switchWindow() when another window gets the - * focus. - */ - public void onUnfocus() { - // Default: do nothing - } - - /** - * Called by application.hideWindow(). + * Called by application.hideWindow(). */ public void onHide() { // Default: do nothing @@ -936,8 +450,13 @@ public class TWindow extends TWidget { } if (mouse.isMouse1() && mouseOnClose()) { - // Close window - application.closeWindow(this); + if ((flags & HIDEONCLOSE) == 0) { + // Close window + application.closeWindow(this); + } else { + // Hide window + application.hideWindow(this); + } return; } @@ -1147,7 +666,13 @@ public class TWindow extends TWidget { // Ctrl-W - close window if (keypress.equals(kbCtrlW)) { if ((flags & NOCLOSEBOX) == 0) { - application.closeWindow(this); + if ((flags & HIDEONCLOSE) == 0) { + // Close window + application.closeWindow(this); + } else { + // Hide window + application.hideWindow(this); + } } return; } @@ -1200,7 +725,13 @@ public class TWindow extends TWidget { if (command.equals(cmWindowClose)) { if ((flags & NOCLOSEBOX) == 0) { - application.closeWindow(this); + if ((flags & HIDEONCLOSE) == 0) { + // Close window + application.closeWindow(this); + } else { + // Hide window + application.hideWindow(this); + } } return; } @@ -1246,7 +777,13 @@ public class TWindow extends TWidget { if (menu.getId() == TMenu.MID_WINDOW_CLOSE) { if ((flags & NOCLOSEBOX) == 0) { - application.closeWindow(this); + if ((flags & HIDEONCLOSE) == 0) { + // Close window + application.closeWindow(this); + } else { + // Hide window + application.hideWindow(this); + } } return; } @@ -1283,6 +820,509 @@ public class TWindow extends TWidget { super.onMenu(menu); } + // ------------------------------------------------------------------------ + // 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(); + + getScreen().drawBox(0, 0, getWidth(), getHeight(), border, + background, borderType, true); + + // Draw the title + int titleLeft = (getWidth() - title.length() - 2) / 2; + putCharXY(titleLeft, 0, ' ', border); + putStringXY(titleLeft + 1, 0, title); + putCharXY(titleLeft + title.length() + 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() - 1); + setX(0); + setY(1); + 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). + */ + public void activate() { + application.activateWindow(this); + } + + /** + * Close window. Note that windows without a close box can still be + * closed by calling the close() method. + */ + 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; + } + + /** + * 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) + ) { + assert (isActive()); + 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; + } + } + // ------------------------------------------------------------------------ // Passthru for Screen functions ------------------------------------------ // ------------------------------------------------------------------------ diff --git a/src/jexer/backend/Backend.java b/src/jexer/backend/Backend.java index d186328..20efa7e 100644 --- a/src/jexer/backend/Backend.java +++ b/src/jexer/backend/Backend.java @@ -60,6 +60,13 @@ public interface Backend { */ 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. diff --git a/src/jexer/backend/ECMA48Backend.java b/src/jexer/backend/ECMA48Backend.java index 59cd670..b99c120 100644 --- a/src/jexer/backend/ECMA48Backend.java +++ b/src/jexer/backend/ECMA48Backend.java @@ -40,6 +40,10 @@ import java.io.UnsupportedEncodingException; */ public final 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; @@ -161,5 +165,4 @@ public final class ECMA48Backend extends GenericBackend { this(listener, input, reader, writer, false); } - } diff --git a/src/jexer/backend/ECMA48Terminal.java b/src/jexer/backend/ECMA48Terminal.java index ae35610..360994a 100644 --- a/src/jexer/backend/ECMA48Terminal.java +++ b/src/jexer/backend/ECMA48Terminal.java @@ -59,6 +59,27 @@ import static jexer.TKeypress.*; public final 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. */ @@ -75,15 +96,6 @@ public final class ECMA48Terminal extends LogicalScreen */ private SessionInfo sessionInfo; - /** - * Getter for sessionInfo. - * - * @return the SessionInfo - */ - public SessionInfo getSessionInfo() { - return sessionInfo; - } - /** * The event queue, filled up by a thread reading on input. */ @@ -103,20 +115,7 @@ public final class ECMA48Terminal extends LogicalScreen * 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 ArrayList params; - - /** - * States in the input parser. - */ - private enum ParseState { - GROUND, - ESCAPE, - ESCAPE_INTERMEDIATE, - CSI_ENTRY, - CSI_PARAM, - MOUSE, - MOUSE_SGR, - } + private List params; /** * Current parsing state. @@ -194,101 +193,9 @@ public final class ECMA48Terminal extends LogicalScreen */ private Object listener; - /** - * 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; - } - - /** - * Get the output writer. - * - * @return the Writer - */ - public PrintWriter getOutput() { - return output; - } - - /** - * 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); - } - } - - /** - * 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) { - e.printStackTrace(); - } - } - int rc = process.exitValue(); - if (rc != 0) { - System.err.println("stty returned error code: " + rc); - } - } catch (IOException e) { - e.printStackTrace(); - } - } + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ /** * Constructor sets up state for getEvent(). @@ -517,6 +424,73 @@ public final class ECMA48Terminal extends LogicalScreen 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() { + String result = flushString(); + if ((cursorVisible) + && (cursorY >= 0) + && (cursorX >= 0) + && (cursorY <= height - 1) + && (cursorX <= width - 1) + ) { + result += cursor(true); + result += gotoXY(cursorX, cursorY); + } else { + result += cursor(false); + } + output.write(result); + 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. */ @@ -558,6 +532,195 @@ public final class ECMA48Terminal extends LogicalScreen } } + /** + * 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; + } + + // ------------------------------------------------------------------------ + // 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 LinkedList(); + + 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(); + } + + // 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)) + // System.err.println("*** run() exiting..."); System.err.flush(); + } + + // ------------------------------------------------------------------------ + // ECMA48Terminal --------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * 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) { + e.printStackTrace(); + } + } + int rc = process.exitValue(); + if (rc != 0) { + System.err.println("stty returned error code: " + rc); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + /** * Flush output. */ @@ -763,37 +926,6 @@ public final class ECMA48Terminal extends LogicalScreen return result; } - /** - * Push the logical screen to the physical device. - */ - @Override - public void flushPhysical() { - String result = flushString(); - if ((cursorVisible) - && (cursorY >= 0) - && (cursorX >= 0) - && (cursorY <= height - 1) - && (cursorX <= width - 1) - ) { - result += cursor(true); - result += gotoXY(cursorX, cursorY); - } else { - result += cursor(false); - } - output.write(result); - flush(); - } - - /** - * Set the window title. - * - * @param title the new title - */ - public void setTitle(final String title) { - output.write(getSetTitleString(title)); - flush(); - } - /** * Reset keyboard/mouse input parser. */ @@ -1108,22 +1240,6 @@ public final class ECMA48Terminal extends LogicalScreen eventMouseWheelUp, eventMouseWheelDown); } - /** - * 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(); - } - } - } - /** * Return any events in the IO queue due to timeout. * @@ -1895,91 +2011,4 @@ public final class ECMA48Terminal extends LogicalScreen return "\033[?1002;1003;1006;1005l\033[?1049l"; } - /** - * 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 LinkedList(); - - 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(); - } - - // 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)) - // System.err.println("*** run() exiting..."); System.err.flush(); - } - } diff --git a/src/jexer/backend/GenericBackend.java b/src/jexer/backend/GenericBackend.java index bf27e94..bb9ae9f 100644 --- a/src/jexer/backend/GenericBackend.java +++ b/src/jexer/backend/GenericBackend.java @@ -39,11 +39,33 @@ import jexer.event.TInputEvent; */ 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; + + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ + + // ------------------------------------------------------------------------ + // Backend ---------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Getter for sessionInfo. * @@ -53,11 +75,6 @@ public abstract class GenericBackend implements Backend { return sessionInfo; } - /** - * The screen to draw on. - */ - protected Screen screen; - /** * Getter for screen. * @@ -75,9 +92,13 @@ public abstract class GenericBackend implements Backend { } /** - * Input events are processed by this Terminal. + * Check if there are events in the queue. + * + * @return if true, getEvents() has something to return to the application */ - protected TerminalReader terminal; + public boolean hasEvents() { + return terminal.hasEvents(); + } /** * Get keyboard, mouse, and screen resize events. diff --git a/src/jexer/backend/LogicalScreen.java b/src/jexer/backend/LogicalScreen.java index c9b025a..c24703e 100644 --- a/src/jexer/backend/LogicalScreen.java +++ b/src/jexer/backend/LogicalScreen.java @@ -37,6 +37,10 @@ import jexer.bits.GraphicsChars; */ public class LogicalScreen implements Screen { + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Width of the visible window. */ @@ -52,6 +56,84 @@ public class LogicalScreen implements Screen { */ 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; + + // ------------------------------------------------------------------------ + // 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 ----------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Set drawing offset for x. * @@ -61,11 +143,6 @@ public class LogicalScreen implements Screen { this.offsetX = offsetX; } - /** - * Drawing offset for y. - */ - private int offsetY; - /** * Set drawing offset for y. * @@ -75,11 +152,6 @@ public class LogicalScreen implements Screen { this.offsetY = offsetY; } - /** - * Ignore anything drawn right of clipRight. - */ - private int clipRight; - /** * Get right drawing clipping boundary. * @@ -98,11 +170,6 @@ public class LogicalScreen implements Screen { this.clipRight = clipRight; } - /** - * Ignore anything drawn below clipBottom. - */ - private int clipBottom; - /** * Get bottom drawing clipping boundary. * @@ -121,11 +188,6 @@ public class LogicalScreen implements Screen { this.clipBottom = clipBottom; } - /** - * Ignore anything drawn left of clipLeft. - */ - private int clipLeft; - /** * Get left drawing clipping boundary. * @@ -144,11 +206,6 @@ public class LogicalScreen implements Screen { this.clipLeft = clipLeft; } - /** - * Ignore anything drawn above clipTop. - */ - private int clipTop; - /** * Get top drawing clipping boundary. * @@ -167,16 +224,6 @@ public class LogicalScreen implements Screen { this.clipTop = clipTop; } - /** - * The physical screen last sent out on flush(). - */ - protected Cell [][] physical; - - /** - * The logical screen being rendered to. - */ - protected Cell [][] logical; - /** * Get dirty flag. * @@ -200,28 +247,6 @@ public class LogicalScreen implements Screen { return false; } - /** - * 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; - /** * Get the attributes at one location. * @@ -491,50 +516,6 @@ public class LogicalScreen implements Screen { } } - /** - * 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; - } - /** * Change the width. Everything on-screen will be destroyed and must be * redrawn. @@ -584,19 +565,6 @@ public class LogicalScreen implements Screen { return this.width; } - /** - * 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); - } - /** * Reset screen to not-bold, white-on-black. Also flushes the offset and * clip variables. @@ -629,17 +597,6 @@ public class LogicalScreen implements Screen { reset(); } - /** - * 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].reset(); - } - } - } - /** * Draw a box with a border and empty background. * @@ -852,4 +809,63 @@ public class LogicalScreen implements Screen { */ 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].reset(); + } + } + } + } diff --git a/src/jexer/backend/MultiBackend.java b/src/jexer/backend/MultiBackend.java index 9166e1c..4e82d4e 100644 --- a/src/jexer/backend/MultiBackend.java +++ b/src/jexer/backend/MultiBackend.java @@ -38,6 +38,10 @@ import jexer.event.TInputEvent; */ public class MultiBackend implements Backend { + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * The screen to use. */ @@ -48,6 +52,10 @@ public class MultiBackend implements Backend { */ private List backends = new LinkedList(); + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Public constructor requires one backend. Note that this backend's * screen will be replaced with a MultiScreen. @@ -63,35 +71,9 @@ public class MultiBackend implements Backend { } } - /** - * 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()); - } - } - - /** - * 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); - } - } + // ------------------------------------------------------------------------ + // Backend ---------------------------------------------------------------- + // ------------------------------------------------------------------------ /** * Getter for sessionInfo. @@ -121,6 +103,20 @@ public class MultiBackend implements Backend { } } + /** + * Check if there are events in the queue. + * + * @return if true, getEvents() has something to return to the application + */ + public boolean hasEvents() { + for (Backend backend: backends) { + if (backend.hasEvents()) { + return true; + } + } + return false; + } + /** * Subclasses must provide an implementation to get keyboard, mouse, and * screen resize events. @@ -166,4 +162,38 @@ public class MultiBackend implements Backend { } } + // ------------------------------------------------------------------------ + // 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()); + } + } + + /** + * 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 index 735faac..7768873 100644 --- a/src/jexer/backend/MultiScreen.java +++ b/src/jexer/backend/MultiScreen.java @@ -39,11 +39,19 @@ import jexer.bits.CellAttributes; */ public class MultiScreen implements Screen { + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * The list of screens to use. */ private List screens = new LinkedList(); + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Public constructor requires one screen. * @@ -53,25 +61,9 @@ public class MultiScreen implements Screen { screens.add(screen); } - /** - * 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); - } - } + // ------------------------------------------------------------------------ + // Screen ----------------------------------------------------------------- + // ------------------------------------------------------------------------ /** * Set drawing offset for x. @@ -574,4 +566,28 @@ public class MultiScreen implements Screen { } } + // ------------------------------------------------------------------------ + // 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); + } + } + } diff --git a/src/jexer/backend/SwingBackend.java b/src/jexer/backend/SwingBackend.java index a98627e..d6e0742 100644 --- a/src/jexer/backend/SwingBackend.java +++ b/src/jexer/backend/SwingBackend.java @@ -37,6 +37,10 @@ import javax.swing.JComponent; */ public final class SwingBackend extends GenericBackend { + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Public constructor. The window will be 80x25 with font size 20 pts. */ @@ -127,6 +131,10 @@ public final class SwingBackend extends GenericBackend { screen = (SwingTerminal) terminal; } + // ------------------------------------------------------------------------ + // SwingBackend ----------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Set to a new font, and resize the screen to match its dimensions. * diff --git a/src/jexer/backend/SwingComponent.java b/src/jexer/backend/SwingComponent.java index 48e4a44..4b0b2b4 100644 --- a/src/jexer/backend/SwingComponent.java +++ b/src/jexer/backend/SwingComponent.java @@ -52,11 +52,53 @@ import javax.swing.JFrame; */ 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; + + // ------------------------------------------------------------------------ + // 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. * @@ -73,16 +115,6 @@ class SwingComponent { } } - /** - * 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; - /** * Get the JFrame reference. * @@ -101,26 +133,6 @@ class SwingComponent { return component; } - /** - * 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(); - } - /** * Setup to render to an existing JComponent. */ diff --git a/src/jexer/backend/SwingSessionInfo.java b/src/jexer/backend/SwingSessionInfo.java index 7457f57..b4c0b59 100644 --- a/src/jexer/backend/SwingSessionInfo.java +++ b/src/jexer/backend/SwingSessionInfo.java @@ -37,6 +37,10 @@ import java.awt.Insets; */ public final class SwingSessionInfo implements SessionInfo { + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * The Swing JFrame or JComponent. */ @@ -72,6 +76,48 @@ public final class SwingSessionInfo implements SessionInfo { */ 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. * @@ -126,53 +172,6 @@ public final class SwingSessionInfo implements SessionInfo { return windowHeight; } - /** - * 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; - } - - /** - * 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; - } - /** * Re-query the text window size. */ @@ -191,4 +190,21 @@ public final class SwingSessionInfo implements SessionInfo { } + // ------------------------------------------------------------------------ + // 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; + } + } diff --git a/src/jexer/backend/SwingTerminal.java b/src/jexer/backend/SwingTerminal.java index a980638..598f0ab 100644 --- a/src/jexer/backend/SwingTerminal.java +++ b/src/jexer/backend/SwingTerminal.java @@ -54,8 +54,10 @@ import java.io.InputStream; import java.util.HashMap; import java.util.LinkedList; 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; @@ -84,14 +86,19 @@ public final class SwingTerminal extends LogicalScreen MouseListener, MouseMotionListener, MouseWheelListener, WindowListener { + // ------------------------------------------------------------------------ + // Constants -------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** - * The Swing component or frame to draw to. + * The icon image location. */ - private SwingComponent swing; + private static final String ICONFILE = "jexer_logo_128.png"; - // ------------------------------------------------------------------------ - // Screen ----------------------------------------------------------------- - // ------------------------------------------------------------------------ + /** + * The terminus font resource filename. + */ + private static final String FONTFILE = "terminus-ttf-4.39/TerminusTTF-Bold-4.39.ttf"; /** * Cursor style to draw. @@ -113,17 +120,9 @@ public final class SwingTerminal extends LogicalScreen OUTLINE } - /** - * 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; + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ // Colors to map DOS colors to AWT colors. private static Color MYBLACK; @@ -149,36 +148,21 @@ public final class SwingTerminal extends LogicalScreen private static boolean dosColors = false; /** - * Setup Swing colors to match DOS color palette. + * The Swing component or frame to draw to. */ - 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); + private SwingComponent swing; - dosColors = true; - } + /** + * A cache of previously-rendered glyphs for blinking text, when it is + * not visible. + */ + private Map glyphCacheBlink; /** - * The terminus font resource filename. + * A cache of previously-rendered glyphs for non-blinking, or + * blinking-and-visible, text. */ - private static final String FONTFILE = "terminus-ttf-4.39/TerminusTTF-Bold-4.39.ttf"; + private Map glyphCache; /** * If true, we were successful getting Terminus. @@ -246,17 +230,6 @@ public final class SwingTerminal extends LogicalScreen */ private long blinkMillis = 500; - /** - * 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; - } - /** * If true, the cursor should be visible right now based on the blink * time. @@ -270,507 +243,321 @@ public final class SwingTerminal extends LogicalScreen private long lastBlinkTime = 0; /** - * Get the font size in points. - * - * @return font size in points + * The session information. */ - public int getFontSize() { - return fontSize; - } + private SwingSessionInfo sessionInfo; /** - * Set the font size in points. - * - * @param fontSize font size in points + * The listening object that run() wakes up on new input. */ - public void setFontSize(final int fontSize) { - this.fontSize = fontSize; - Font newFont = font.deriveFont((float) fontSize); - setFont(newFont); - } + private Object listener; /** - * Set to a new font, and resize the screen to match its dimensions. - * - * @param font the new font + * The event queue, filled up by a thread reading on input. */ - public void setFont(final Font font) { - this.font = font; - getFontDimensions(); - swing.setFont(font); - glyphCacheBlink = new HashMap(); - glyphCache = new HashMap(); - resizeToScreen(); - } + private List eventQueue; /** - * Set the font to Terminus, the best all-around font for both CP437 and - * ISO8859-1. + * The last reported mouse X position. */ - public void getDefaultFont() { - 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); - gotTerminus = true; - font = terminus; - } catch (Exception e) { - e.printStackTrace(); - font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize); - } - - setFont(font); - } + private int oldMouseX = -1; /** - * Convert a CellAttributes foreground color to an Swing Color. - * - * @param attr the text attributes - * @return the Swing Color + * The last reported mouse Y position. */ - private Color attrToForegroundColor(final CellAttributes attr) { - 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()); - } + private int oldMouseY = -1; /** - * Convert a CellAttributes background color to an Swing Color. - * - * @param attr the text attributes - * @return the Swing Color + * true if mouse1 was down. Used to report mouse1 on the release event. */ - private Color attrToBackgroundColor(final CellAttributes attr) { - 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()); - } + private boolean mouse1 = false; /** - * Figure out what textAdjustX and textAdjustY should be, based on the - * location of a vertical bar (to find textAdjustY) and a horizontal bar - * (to find textAdjustX). - * - * @return true if textAdjustX and textAdjustY were guessed at correctly + * true if mouse2 was down. Used to report mouse2 on the release event. */ - private boolean getFontAdjustments() { - BufferedImage image = null; + private boolean mouse2 = false; - // 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. + /** + * true if mouse3 was down. Used to report mouse3 on the release event. + */ + private boolean mouse3 = false; - Graphics2D gr2 = null; - int gr2x = 3; - int gr2y = 3; - image = new BufferedImage(textWidth * 2, textHeight * 2, - BufferedImage.TYPE_INT_ARGB); + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ - gr2 = image.createGraphics(); - gr2.setFont(swing.getFont()); - gr2.setColor(java.awt.Color.BLACK); - gr2.fillRect(0, 0, textWidth * 2, textHeight * 2); - gr2.setColor(java.awt.Color.WHITE); - char [] chars = new char[1]; - chars[0] = jexer.bits.GraphicsChars.VERTICAL_BAR; - gr2.drawChars(chars, 0, 1, gr2x, gr2y + textHeight - maxDescent); - gr2.dispose(); + /** + * 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) { - for (int x = 0; x < textWidth; x++) { - for (int y = 0; y < textHeight; y++) { + this.fontSize = fontSize; - /* - System.err.println("X: " + x + " Y: " + y + " " + - image.getRGB(x, y)); - */ + setDOSColors(); - if ((image.getRGB(x, y) & 0xFFFFFF) != 0) { - textAdjustY = (gr2y - y); + // 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; + } - // System.err.println("textAdjustY: " + textAdjustY); - x = textWidth; - break; - } + // Pull the system property for triple buffering. + if (System.getProperty("jexer.Swing.tripleBuffer") != null) { + if (System.getProperty("jexer.Swing.tripleBuffer").equals("true")) { + SwingComponent.tripleBuffer = true; + } else { + SwingComponent.tripleBuffer = false; } } - gr2 = image.createGraphics(); - gr2.setFont(swing.getFont()); - gr2.setColor(java.awt.Color.BLACK); - gr2.fillRect(0, 0, textWidth * 2, textHeight * 2); - gr2.setColor(java.awt.Color.WHITE); - chars[0] = jexer.bits.GraphicsChars.SINGLE_BAR; - gr2.drawChars(chars, 0, 1, gr2x, gr2y + textHeight - maxDescent); - gr2.dispose(); + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { - for (int x = 0; x < textWidth; x++) { - for (int y = 0; y < textHeight; y++) { + JFrame frame = new JFrame() { - /* - System.err.println("X: " + x + " Y: " + y + " " + - image.getRGB(x, y)); - */ + /** + * Serializable version. + */ + private static final long serialVersionUID = 1; - if ((image.getRGB(x, y) & 0xFFFFFF) != 0) { - textAdjustX = (gr2x - x); + /** + * The code that performs the actual drawing. + */ + public SwingTerminal screen = null; - // System.err.println("textAdjustX: " + textAdjustX); - return true; - } - } - } + /* + * Anonymous class initializer saves the screen + * reference, so that paint() and the like call out + * to SwingTerminal. + */ + { + this.screen = SwingTerminal.this; + } - // Something weird happened, don't rely on this function. - // System.err.println("getFontAdjustments: false"); - return false; - } + /** + * 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); + } - /** - * 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); - } + /** + * Paint redraws the whole screen. + * + * @param gr the Swing Graphics context + */ + @Override + public void paint(final Graphics gr) { + if (screen != null) { + screen.paint(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(); - textWidth = (int)Math.round(bounds.getWidth()); - // textHeight = (int)Math.round(bounds.getHeight()) - maxDescent; + // Set icon + ClassLoader loader = Thread.currentThread(). + getContextClassLoader(); + frame.setIconImage((new ImageIcon(loader. + getResource(ICONFILE))).getImage()); - // This produces the same number, but works better for ugly - // monospace. - textHeight = fm.getMaxAscent() + maxDescent - leading; + // Get the Swing component + SwingTerminal.this.swing = new SwingComponent(frame); - if (gotTerminus == true) { - textHeight++; - } + // Hang onto top and left for drawing. + Insets insets = SwingTerminal.this.swing.getInsets(); + SwingTerminal.this.left = insets.left; + SwingTerminal.this.top = insets.top; - if (getFontAdjustments() == false) { - // We were unable to programmatically determine textAdjustX and - // textAdjustY, so try some guesses based on VM vendor. - String runtime = System.getProperty("java.runtime.name"); - if ((runtime != null) && (runtime.contains("Java(TM)"))) { - textAdjustY = -1; - textAdjustX = 0; - } - } + // Load the font so that we can set sessionInfo. + getDefaultFont(); - if (sessionInfo != null) { - sessionInfo.setTextCellDimensions(textWidth, textHeight); + // 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(); + SwingTerminal.this.swing.setVisible(true); + } + }); + } catch (Exception e) { + e.printStackTrace(); } - gotFontDimensions = true; - } - /** - * Resize to font dimensions. - */ - public void resizeToScreen() { - swing.setDimensions(textWidth * width, textHeight * height); + this.listener = listener; + mouse1 = false; + mouse2 = false; + mouse3 = false; + eventQueue = new LinkedList(); + + // Add listeners to Swing. + swing.addKeyListener(this); + swing.addWindowListener(this); + swing.addComponentListener(this); + swing.addMouseListener(this); + swing.addMouseMotionListener(this); + swing.addMouseWheelListener(this); } /** - * Draw one glyph to the screen. + * Public constructor renders to an existing JComponent. * - * @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. + * @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 */ - private void drawGlyph(final Graphics gr, final Cell cell, - final int xPixel, final int yPixel) { + public SwingTerminal(final JComponent component, final int windowWidth, + final int windowHeight, final int fontSize, final Object listener) { - /* - System.err.println("drawGlyph(): " + xPixel + " " + yPixel + - " " + cell); - */ + this.fontSize = fontSize; - 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; - } + setDOSColors(); - // 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; + // 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; } - Cell cellColor = new Cell(); - cellColor.setTo(cell); + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { - // Check for reverse - if (cell.isReverse()) { - cellColor.setForeColor(cell.getBackColor()); - cellColor.setBackColor(cell.getForeColor()); - } + JComponent newComponent = new JComponent() { - // Draw the background rectangle, then the foreground character. - gr2.setColor(attrToBackgroundColor(cellColor)); - gr2.fillRect(gr2x, gr2y, textWidth, textHeight); + /** + * Serializable version. + */ + private static final long serialVersionUID = 1; - // Handle blink and underline - if (!cell.isBlink() - || (cell.isBlink() && cursorBlinkVisible) - ) { - gr2.setColor(attrToForegroundColor(cellColor)); - char [] chars = new char[1]; - chars[0] = cell.getChar(); - gr2.drawChars(chars, 0, 1, gr2x + textAdjustX, - gr2y + textHeight - maxDescent + textAdjustY); + /** + * The code that performs the actual drawing. + */ + public SwingTerminal screen = null; - 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(); - key.setTo(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()); - } - } - - } + /* + * Anonymous class initializer saves the screen + * reference, so that paint() and the like call out + * to SwingTerminal. + */ + { + this.screen = SwingTerminal.this; + } - /** - * Check if the cursor is visible, and if so draw it. - * - * @param gr the Swing Graphics context - */ - private void drawCursor(final Graphics gr) { + /** + * 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); + } - 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]; - gr.setColor(attrToForegroundColor(lCell)); - switch (cursorStyle) { - default: - // Fall through... - case UNDERLINE: - gr.fillRect(xPixel, yPixel + textHeight - 2, textWidth, 2); - break; - case BLOCK: - gr.fillRect(xPixel, yPixel, textWidth, textHeight); - break; - case OUTLINE: - gr.drawRect(xPixel, yPixel, textWidth - 1, textHeight - 1); - break; - } - } - } + /** + * 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); - /** - * Reset the blink timer. - */ - private void resetBlinkTimer() { - lastBlinkTime = System.currentTimeMillis(); - cursorBlinkVisible = true; - } + // Allow key events to be received + component.setFocusable(true); - /** - * Paint redraws the whole screen. - * - * @param gr the Swing Graphics context - */ - public void paint(final Graphics gr) { + // Get the Swing component + SwingTerminal.this.swing = new SwingComponent(component); - 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); - */ - } + // Hang onto top and left for drawing. + Insets insets = SwingTerminal.this.swing.getInsets(); + SwingTerminal.this.left = insets.left; + SwingTerminal.this.top = insets.top; - int xCellMin = 0; - int xCellMax = width; - int yCellMin = 0; - int yCellMax = height; + // Load the font so that we can set sessionInfo. + getDefaultFont(); - Rectangle bounds = gr.getClipBounds(); - if (bounds != null) { - // Only update what is in the bounds - xCellMin = textColumn(bounds.x); - xCellMax = textColumn(bounds.x + bounds.width); - 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); - if (yCellMax > height) { - yCellMax = height; - } - if (yCellMin >= yCellMax) { - yCellMin = yCellMax - 2; - } - if (yCellMin < 0) { - yCellMin = 0; - } - } else { - // We need a total repaint - reallyCleared = true; + // 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 (Exception e) { + e.printStackTrace(); } - // 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)) { - - drawGlyph(gr, lCell, xPixel, yPixel); - - // Physical is always updated - physical[x][y].setTo(lCell); - } - } - } - drawCursor(gr); + this.listener = listener; + mouse1 = false; + mouse2 = false; + mouse3 = false; + eventQueue = new LinkedList(); - reallyCleared = false; - } // synchronized (this) + // Add listeners to Swing. + swing.addKeyListener(this); + swing.addWindowListener(this); + swing.addComponentListener(this); + swing.addMouseListener(this); + swing.addMouseMotionListener(this); + swing.addMouseWheelListener(this); } + // ------------------------------------------------------------------------ + // LogicalScreen ---------------------------------------------------------- + // ------------------------------------------------------------------------ + /** - * Restore terminal to normal state. + * Set the window title. + * + * @param title the new title */ - public void shutdown() { - swing.dispose(); + @Override + public void setTitle(final String title) { + swing.setTitle(title); } /** @@ -804,520 +591,787 @@ public final class SwingTerminal extends LogicalScreen } } + // ------------------------------------------------------------------------ + // TerminalReader --------------------------------------------------------- + // ------------------------------------------------------------------------ + /** - * Push the logical screen to the physical device. + * Check if there are events in the queue. + * + * @return if true, getEvents() has something to return to the backend */ - private void drawToSwing() { + public boolean hasEvents() { + synchronized (eventQueue) { + return (eventQueue.size() > 0); + } + } - /* - System.err.printf("drawToSwing(): reallyCleared %s dirty %s\n", - reallyCleared, dirty); - */ + /** + * 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(); + } + } + } - // 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(); + /** + * 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; + } + + // ------------------------------------------------------------------------ + // SwingTerminal ---------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * 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); - if ((swing.getFrame() != null) && (swing.getBufferStrategy() != null)) { - Graphics gr = swing.getBufferStrategy().getDrawGraphics(); + dosColors = true; + } - 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]; + /** + * 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; + } - int xPixel = x * textWidth + left; - int yPixel = y * textHeight + top; + /** + * Get the font size in points. + * + * @return font size in points + */ + public int getFontSize() { + return fontSize; + } - if (!lCell.equals(pCell) - || ((x == cursorX) - && (y == cursorY) - && cursorVisible) - || (lCell.isBlink()) - ) { - drawGlyph(gr, lCell, xPixel, yPixel); - physical[x][y].setTo(lCell); - } - } - } - drawCursor(gr); - } // synchronized (this) + /** + * 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); + } - gr.dispose(); - swing.getBufferStrategy().show(); - Toolkit.getDefaultToolkit().sync(); - return; + /** + * Set to a new font, and resize the screen to match its dimensions. + * + * @param font the new font + */ + public void setFont(final Font font) { + this.font = font; + getFontDimensions(); + swing.setFont(font); + glyphCacheBlink = new HashMap(); + glyphCache = new HashMap(); + resizeToScreen(); + } + + /** + * Set the font to Terminus, the best all-around font for both CP437 and + * ISO8859-1. + */ + public void getDefaultFont() { + 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); + gotTerminus = true; + font = terminus; + } catch (Exception e) { + e.printStackTrace(); + font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize); + } + + setFont(font); + } + + /** + * Convert a CellAttributes foreground color to an Swing Color. + * + * @param attr the text attributes + * @return the Swing Color + */ + private Color attrToForegroundColor(final CellAttributes attr) { + 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 + */ + private Color attrToBackgroundColor(final CellAttributes attr) { + 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 and textAdjustY should be, based on the + * location of a vertical bar (to find textAdjustY) and a horizontal bar + * (to find textAdjustX). + * + * @return true if textAdjustX and textAdjustY were guessed at correctly + */ + private boolean 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(textWidth * 2, textHeight * 2, + BufferedImage.TYPE_INT_ARGB); + + gr2 = image.createGraphics(); + gr2.setFont(swing.getFont()); + gr2.setColor(java.awt.Color.BLACK); + gr2.fillRect(0, 0, textWidth * 2, textHeight * 2); + gr2.setColor(java.awt.Color.WHITE); + char [] chars = new char[1]; + chars[0] = jexer.bits.GraphicsChars.VERTICAL_BAR; + gr2.drawChars(chars, 0, 1, gr2x, gr2y + textHeight - maxDescent); + gr2.dispose(); + + for (int x = 0; x < textWidth; x++) { + for (int y = 0; y < textHeight; y++) { + + /* + System.err.println("X: " + x + " Y: " + y + " " + + image.getRGB(x, y)); + */ + + if ((image.getRGB(x, y) & 0xFFFFFF) != 0) { + textAdjustY = (gr2y - y); + + // System.err.println("textAdjustY: " + textAdjustY); + x = textWidth; + break; + } + } } - // Swing thread version: request a repaint, but limit it to the area - // that has changed. + gr2 = image.createGraphics(); + gr2.setFont(swing.getFont()); + gr2.setColor(java.awt.Color.BLACK); + gr2.fillRect(0, 0, textWidth * 2, textHeight * 2); + gr2.setColor(java.awt.Color.WHITE); + chars[0] = jexer.bits.GraphicsChars.SINGLE_BAR; + gr2.drawChars(chars, 0, 1, gr2x, gr2y + textHeight - maxDescent); + gr2.dispose(); - // Find the minimum-size damaged region. - int xMin = swing.getWidth(); - int xMax = 0; - int yMin = swing.getHeight(); - int yMax = 0; + for (int x = 0; x < textWidth; x++) { + for (int y = 0; y < textHeight; y++) { - 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]; + /* + System.err.println("X: " + x + " Y: " + y + " " + + image.getRGB(x, y)); + */ - int xPixel = x * textWidth + left; - int yPixel = y * textHeight + top; + if ((image.getRGB(x, y) & 0xFFFFFF) != 0) { + textAdjustX = (gr2x - x); - 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; - } - } + // System.err.println("textAdjustX: " + textAdjustX); + return true; } } } - 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); - } + // Something weird happened, don't rely on this function. + // System.err.println("getFontAdjustments: false"); + return false; } /** - * Convert pixel column position to text cell column position. - * - * @param x pixel column position - * @return text cell column position + * Figure out my font dimensions. This code path works OK for the JFrame + * case, and can be called immediately after JFrame creation. */ - public int textColumn(final int x) { - return ((x - left) / textWidth); + private void getFontDimensions() { + swing.setFont(font); + Graphics gr = swing.getGraphics(); + if (gr == null) { + return; + } + getFontDimensions(gr); } /** - * Convert pixel row position to text cell row position. + * Figure out my font dimensions. This code path is needed to lazy-load + * the information inside paint(). * - * @param y pixel row position - * @return text cell row position + * @param gr Graphics object to use */ - public int textRow(final int y) { - return ((y - top) / textHeight); - } + 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(); + textWidth = (int)Math.round(bounds.getWidth()); + // textHeight = (int)Math.round(bounds.getHeight()) - maxDescent; - /** - * Set the window title. - * - * @param title the new title - */ - public void setTitle(final String title) { - swing.setTitle(title); - } + // This produces the same number, but works better for ugly + // monospace. + textHeight = fm.getMaxAscent() + maxDescent - leading; - // ------------------------------------------------------------------------ - // TerminalReader --------------------------------------------------------- - // ------------------------------------------------------------------------ + if (gotTerminus == true) { + textHeight++; + } - /** - * The session information. - */ - private SwingSessionInfo sessionInfo; + if (getFontAdjustments() == false) { + // We were unable to programmatically determine textAdjustX and + // textAdjustY, so try some guesses based on VM vendor. + String runtime = System.getProperty("java.runtime.name"); + if ((runtime != null) && (runtime.contains("Java(TM)"))) { + textAdjustY = -1; + textAdjustX = 0; + } + } - /** - * Getter for sessionInfo. - * - * @return the SessionInfo - */ - public SessionInfo getSessionInfo() { - return sessionInfo; + if (sessionInfo != null) { + sessionInfo.setTextCellDimensions(textWidth, textHeight); + } + gotFontDimensions = true; } /** - * The listening object that run() wakes up on new input. + * Resize to font dimensions. */ - private Object listener; + public void resizeToScreen() { + swing.setDimensions(textWidth * width, textHeight * height); + } /** - * Set listener to a different Object. + * Draw one glyph to the screen. * - * @param listener the new listening object that run() wakes up on new - * input + * @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. */ - public void setListener(final Object listener) { - this.listener = listener; - } + private void drawGlyph(final Graphics gr, final Cell cell, + final int xPixel, final int yPixel) { - /** - * The event queue, filled up by a thread reading on input. - */ - private List eventQueue; + /* + 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(); + cellColor.setTo(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 = new char[1]; + chars[0] = cell.getChar(); + gr2.drawChars(chars, 0, 1, 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(); + key.setTo(cell); + if (cell.isBlink() && !cursorBlinkVisible) { + glyphCacheBlink.put(key, image); + } else { + glyphCache.put(key, image); + } - /** - * The last reported mouse X position. - */ - private int oldMouseX = -1; + if (swing.getFrame() != null) { + gr.drawImage(image, xPixel, yPixel, swing.getFrame()); + } else { + gr.drawImage(image, xPixel, yPixel, swing.getComponent()); + } + } - /** - * The last reported mouse Y position. - */ - private int oldMouseY = -1; + } /** - * true if mouse1 was down. Used to report mouse1 on the release event. + * Check if the cursor is visible, and if so draw it. + * + * @param gr the Swing Graphics context */ - private boolean mouse1 = false; + private void drawCursor(final Graphics gr) { - /** - * true if mouse2 was down. Used to report mouse2 on the release event. - */ - private boolean mouse2 = false; + 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]; + gr.setColor(attrToForegroundColor(lCell)); + switch (cursorStyle) { + default: + // Fall through... + case UNDERLINE: + gr.fillRect(xPixel, yPixel + textHeight - 2, textWidth, 2); + break; + case BLOCK: + gr.fillRect(xPixel, yPixel, textWidth, textHeight); + break; + case OUTLINE: + gr.drawRect(xPixel, yPixel, textWidth - 1, textHeight - 1); + break; + } + } + } /** - * true if mouse3 was down. Used to report mouse3 on the release event. + * Reset the blink timer. */ - private boolean mouse3 = false; + private void resetBlinkTimer() { + lastBlinkTime = System.currentTimeMillis(); + cursorBlinkVisible = true; + } /** - * Public constructor creates a new JFrame to render to. + * Paint redraws the whole screen. * - * @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 + * @param gr the Swing Graphics context */ - public SwingTerminal(final int windowWidth, final int windowHeight, - final int fontSize, final Object listener) { - - this.fontSize = fontSize; - - setDOSColors(); + public void paint(final Graphics gr) { - // 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; + 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); + */ } - // Pull the system property for triple buffering. - if (System.getProperty("jexer.Swing.tripleBuffer") != null) { - if (System.getProperty("jexer.Swing.tripleBuffer").equals("true")) { - SwingComponent.tripleBuffer = true; - } else { - SwingComponent.tripleBuffer = false; - } + if ((swing.getBufferStrategy() != null) + && (SwingUtilities.isEventDispatchThread()) + ) { + // System.err.println("paint(), skip first paint on swing thread"); + return; } - 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; + int xCellMin = 0; + int xCellMax = width; + int yCellMin = 0; + int yCellMax = height; - /* - * Anonymous class initializer saves the screen - * reference, so that paint() and the like call out - * to SwingTerminal. - */ - { - this.screen = SwingTerminal.this; - } + Rectangle bounds = gr.getClipBounds(); + if (bounds != null) { + // Only update what is in the bounds + xCellMin = textColumn(bounds.x); + xCellMax = textColumn(bounds.x + bounds.width); + 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); + if (yCellMax > height) { + yCellMax = height; + } + if (yCellMin >= yCellMax) { + yCellMin = yCellMax - 2; + } + if (yCellMin < 0) { + yCellMin = 0; + } + } else { + // We need a total repaint + reallyCleared = true; + } - /** - * 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); - } + // Prevent updates to the screen's data from the TApplication + // threads. + synchronized (this) { - /** - * Paint redraws the whole screen. - * - * @param gr the Swing Graphics context - */ - @Override - public void paint(final Graphics gr) { - if (screen != null) { - screen.paint(gr); - } - } - }; + /* + System.err.printf("bounds %s X %d %d Y %d %d\n", + bounds, xCellMin, xCellMax, yCellMin, yCellMax); + */ - // Get the Swing component - SwingTerminal.this.swing = new SwingComponent(frame); + for (int y = yCellMin; y < yCellMax; y++) { + for (int x = xCellMin; x < xCellMax; x++) { - // Hang onto top and left for drawing. - Insets insets = SwingTerminal.this.swing.getInsets(); - SwingTerminal.this.left = insets.left; - SwingTerminal.this.top = insets.top; + int xPixel = x * textWidth + left; + int yPixel = y * textHeight + top; - // Load the font so that we can set sessionInfo. - getDefaultFont(); + Cell lCell = logical[x][y]; + Cell pCell = physical[x][y]; - // 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); + if (!lCell.equals(pCell) + || lCell.isBlink() + || reallyCleared + || (swing.getFrame() == null)) { - SwingTerminal.this.setDimensions(sessionInfo.getWindowWidth(), - sessionInfo.getWindowHeight()); + drawGlyph(gr, lCell, xPixel, yPixel); - SwingTerminal.this.resizeToScreen(); - SwingTerminal.this.swing.setVisible(true); + // Physical is always updated + physical[x][y].setTo(lCell); + } } - }); - } catch (Exception e) { - e.printStackTrace(); - } - - this.listener = listener; - mouse1 = false; - mouse2 = false; - mouse3 = false; - eventQueue = new LinkedList(); + } + drawCursor(gr); - // Add listeners to Swing. - swing.addKeyListener(this); - swing.addWindowListener(this); - swing.addComponentListener(this); - swing.addMouseListener(this); - swing.addMouseMotionListener(this); - swing.addMouseWheelListener(this); + reallyCleared = false; + } // synchronized (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 + * Restore terminal to normal state. */ - public SwingTerminal(final JComponent component, final int windowWidth, - final int windowHeight, final int fontSize, final Object listener) { + public void shutdown() { + swing.dispose(); + } - this.fontSize = fontSize; + /** + * Push the logical screen to the physical device. + */ + private void drawToSwing() { - setDOSColors(); + /* + System.err.printf("drawToSwing(): reallyCleared %s dirty %s\n", + reallyCleared, dirty); + */ - // 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; + // 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; } - try { - SwingUtilities.invokeAndWait(new Runnable() { - public void run() { - - JComponent newComponent = new JComponent() { - - /** - * Serializable version. - */ - private static final long serialVersionUID = 1; + if ((swing.getFrame() != null) && (swing.getBufferStrategy() != null)) { + Graphics gr = swing.getBufferStrategy().getDrawGraphics(); - /** - * The code that performs the actual drawing. - */ - public SwingTerminal screen = null; + 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]; - /* - * Anonymous class initializer saves the screen - * reference, so that paint() and the like call out - * to SwingTerminal. - */ - { - this.screen = SwingTerminal.this; - } + int xPixel = x * textWidth + left; + int yPixel = y * textHeight + top; - /** - * 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); + if (!lCell.equals(pCell) + || ((x == cursorX) + && (y == cursorY) + && cursorVisible) + || (lCell.isBlink()) + ) { + drawGlyph(gr, lCell, xPixel, yPixel); + physical[x][y].setTo(lCell); } + } + } + drawCursor(gr); + } // synchronized (this) - /** - * 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); + gr.dispose(); + swing.getBufferStrategy().show(); + Toolkit.getDefaultToolkit().sync(); + return; + } - // Allow key events to be received - component.setFocusable(true); + // Swing thread version: request a repaint, but limit it to the area + // that has changed. - // Get the Swing component - SwingTerminal.this.swing = new SwingComponent(component); + // Find the minimum-size damaged region. + int xMin = swing.getWidth(); + int xMax = 0; + int yMin = swing.getHeight(); + int yMax = 0; - // Hang onto top and left for drawing. - Insets insets = SwingTerminal.this.swing.getInsets(); - SwingTerminal.this.left = insets.left; - SwingTerminal.this.top = insets.top; + 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]; - // Load the font so that we can set sessionInfo. - getDefaultFont(); + int xPixel = x * textWidth + left; + int yPixel = y * textHeight + top; - // 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); + 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; + } + } } - }); - } catch (Exception e) { - e.printStackTrace(); + } + } + if (xMin + textWidth >= xMax) { + xMax += textWidth; + } + if (yMin + textHeight >= yMax) { + yMax += textHeight; } - this.listener = listener; - mouse1 = false; - mouse2 = false; - mouse3 = false; - eventQueue = new LinkedList(); + // Repaint the desired area + /* + System.err.printf("REPAINT X %d %d Y %d %d\n", xMin, xMax, + yMin, yMax); + */ - // Add listeners to Swing. - swing.addKeyListener(this); - swing.addWindowListener(this); - swing.addComponentListener(this); - swing.addMouseListener(this); - swing.addMouseMotionListener(this); - swing.addMouseWheelListener(this); + 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); + } } /** - * Check if there are events in the queue. + * Convert pixel column position to text cell column position. * - * @return if true, getEvents() has something to return to the backend + * @param x pixel column position + * @return text cell column position */ - public boolean hasEvents() { - synchronized (eventQueue) { - return (eventQueue.size() > 0); + 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; } /** - * Return any events in the IO queue. + * Convert pixel row position to text cell row position. * - * @param queue list to append new events to + * @param y pixel row position + * @return text cell row position */ - public void getEvents(final List queue) { - synchronized (eventQueue) { - if (eventQueue.size() > 0) { - synchronized (queue) { - queue.addAll(eventQueue); - } - eventQueue.clear(); - } + 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; } /** - * Restore terminal to normal state. + * Getter for sessionInfo. + * + * @return the SessionInfo */ - public void closeTerminal() { - shutdown(); + public SessionInfo getSessionInfo() { + return sessionInfo; } + // ------------------------------------------------------------------------ + // KeyListener ------------------------------------------------------------ + // ------------------------------------------------------------------------ + /** * Pass Swing keystrokes into the event queue. * @@ -1550,6 +1604,10 @@ public final class SwingTerminal extends LogicalScreen } } + // ------------------------------------------------------------------------ + // WindowListener --------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Pass window events into the event queue. * @@ -1625,6 +1683,10 @@ public final class SwingTerminal extends LogicalScreen // Ignore } + // ------------------------------------------------------------------------ + // ComponentListener ------------------------------------------------------ + // ------------------------------------------------------------------------ + /** * Pass component events into the event queue. * @@ -1672,6 +1734,10 @@ public final class SwingTerminal extends LogicalScreen sessionInfo.getWindowWidth(), sessionInfo.getWindowHeight()); eventQueue.add(windowResize); resetBlinkTimer(); + /* + System.err.println("Add resize event: " + windowResize.getWidth() + + " x " + windowResize.getHeight()); + */ } if (listener != null) { synchronized (listener) { @@ -1680,6 +1746,10 @@ public final class SwingTerminal extends LogicalScreen } } + // ------------------------------------------------------------------------ + // MouseMotionListener ---------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Pass mouse events into the event queue. * @@ -1748,6 +1818,10 @@ public final class SwingTerminal extends LogicalScreen } } + // ------------------------------------------------------------------------ + // MouseListener ---------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Pass mouse events into the event queue. * @@ -1862,6 +1936,10 @@ public final class SwingTerminal extends LogicalScreen } } + // ------------------------------------------------------------------------ + // MouseWheelListener ----------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Pass mouse events into the event queue. * diff --git a/src/jexer/backend/TSessionInfo.java b/src/jexer/backend/TSessionInfo.java index bc32175..63c12f5 100644 --- a/src/jexer/backend/TSessionInfo.java +++ b/src/jexer/backend/TSessionInfo.java @@ -34,6 +34,10 @@ package jexer.backend; */ public final class TSessionInfo implements SessionInfo { + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * User name. */ @@ -54,6 +58,32 @@ public final class TSessionInfo implements SessionInfo { */ 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. * @@ -115,22 +145,4 @@ public final class TSessionInfo implements SessionInfo { // NOP } - /** - * 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; - } - } diff --git a/src/jexer/backend/TTYSessionInfo.java b/src/jexer/backend/TTYSessionInfo.java index 67b2e5c..28b4bd6 100644 --- a/src/jexer/backend/TTYSessionInfo.java +++ b/src/jexer/backend/TTYSessionInfo.java @@ -40,6 +40,10 @@ import java.util.StringTokenizer; */ public final class TTYSessionInfo implements SessionInfo { + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * User name. */ @@ -65,6 +69,24 @@ public final class TTYSessionInfo implements SessionInfo { */ 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. * @@ -101,54 +123,6 @@ public final class TTYSessionInfo implements SessionInfo { this.language = language; } - /** - * 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) { - e.printStackTrace(); - } - } - int rc = process.exitValue(); - if (rc != 0) { - System.err.println("stty returned error code: " + rc); - } - } catch (IOException e) { - e.printStackTrace(); - } - } - /** * Text window width getter. * @@ -199,13 +173,56 @@ public final class TTYSessionInfo implements SessionInfo { } } + // ------------------------------------------------------------------------ + // TTYSessionInfo --------------------------------------------------------- + // ------------------------------------------------------------------------ + /** - * Public constructor. + * Call 'stty size' to obtain the tty window size. windowWidth and + * windowHeight are set automatically. */ - public TTYSessionInfo() { - // Populate lang and user from the environment - username = System.getProperty("user.name"); - language = System.getProperty("user.language"); - queryWindowSize(); + 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) { + e.printStackTrace(); + } + } + 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 index 7652d3f..41809cf 100644 --- a/src/jexer/backend/TWindowBackend.java +++ b/src/jexer/backend/TWindowBackend.java @@ -48,6 +48,10 @@ import jexer.TWindow; */ public class TWindowBackend extends TWindow implements Backend { + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * The listening object that run() wakes up on new input. */ @@ -84,32 +88,9 @@ public class TWindowBackend extends TWindow implements Backend { */ private SessionInfo sessionInfo; - /** - * 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; - } - - /** - * Getter for sessionInfo. - * - * @return the SessionInfo - */ - public final SessionInfo getSessionInfo() { - return sessionInfo; - } + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ /** * Public constructor. Window will be located at (0, 0). @@ -214,97 +195,9 @@ public class TWindowBackend extends TWindow implements Backend { drawLock = otherScreen; } - /** - * Subclasses must provide an implementation that syncs the logical - * screen to the physical device. - */ - public void flushScreen() { - getApplication().doRepaint(); - } - - /** - * 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; - } - - /** - * 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 the mouse pointer is over the other window, draw its - // pointer again here. (Their TApplication drew it, then our - // TApplication drew it again (undo-ing it), so now we draw it a - // third time so that it is visible.) - if ((otherMouseX != -1) && (otherMouseY != -1)) { - CellAttributes attr = getAttrXY(otherMouseX, otherMouseY); - attr.setForeColor(attr.getForeColor().invert()); - attr.setBackColor(attr.getBackColor().invert()); - putAttrXY(otherMouseX, otherMouseY, attr, false); - } - - // 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); - } - } - } - - /** - * Subclasses should override this method to cleanup resources. This is - * called by application.closeWindow(). - */ - public void onClose() { - // TODO: send a screen disconnect - } + // ------------------------------------------------------------------------ + // Event handlers --------------------------------------------------------- + // ------------------------------------------------------------------------ /** * Returns true if the mouse is currently in the otherScreen window. @@ -413,4 +306,147 @@ public class TWindowBackend extends TWindow implements Backend { } } + // ------------------------------------------------------------------------ + // 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 the mouse pointer is over the other window, draw its + // pointer again here. (Their TApplication drew it, then our + // TApplication drew it again (undo-ing it), so now we draw it a + // third time so that it is visible.) + if ((otherMouseX != -1) && (otherMouseY != -1)) { + CellAttributes attr = getAttrXY(otherMouseX, otherMouseY); + attr.setForeColor(attr.getForeColor().invert()); + attr.setBackColor(attr.getBackColor().invert()); + putAttrXY(otherMouseX, otherMouseY, attr, false); + } + + // 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); + } + } + } + + /** + * Subclasses should override this method to cleanup resources. This is + * called by application.closeWindow(). + */ + @Override + public void onClose() { + // TODO: send a screen disconnect + } + + // ------------------------------------------------------------------------ + // 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; + } + + // ------------------------------------------------------------------------ + // 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; + } + } diff --git a/src/jexer/bits/Cell.java b/src/jexer/bits/Cell.java index eaaab6b..56633e7 100644 --- a/src/jexer/bits/Cell.java +++ b/src/jexer/bits/Cell.java @@ -33,11 +33,45 @@ package jexer.bits; */ public final class Cell extends CellAttributes { + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * The character at this cell. */ private char ch; + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Public constructor sets default values of the cell to blank. + * + * @see #isBlank() + * @see #reset() + */ + public Cell() { + reset(); + } + + /** + * Public constructor sets the character. Attributes are the same as + * default. + * + * @param ch character to set to + * @see #reset() + */ + public Cell(final char ch) { + reset(); + this.ch = ch; + } + + // ------------------------------------------------------------------------ + // Cell ------------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Getter for cell character. * @@ -149,28 +183,6 @@ public final class Cell extends CellAttributes { super.setTo(that); } - /** - * Public constructor sets default values of the cell to blank. - * - * @see #isBlank() - * @see #reset() - */ - public Cell() { - reset(); - } - - /** - * Public constructor sets the character. Attributes are the same as - * default. - * - * @param ch character to set to - * @see #reset() - */ - public Cell(final char ch) { - reset(); - this.ch = ch; - } - /** * Make human-readable description of this Cell. * diff --git a/src/jexer/bits/CellAttributes.java b/src/jexer/bits/CellAttributes.java index c145576..ec3f6dd 100644 --- a/src/jexer/bits/CellAttributes.java +++ b/src/jexer/bits/CellAttributes.java @@ -33,11 +33,73 @@ package jexer.bits; */ public class CellAttributes { + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Bold attribute. */ private boolean bold; + /** + * Blink attribute. + */ + private boolean blink; + + /** + * Reverse attribute. + */ + private boolean reverse; + + /** + * Underline attribute. + */ + private boolean underline; + + /** + * Protected attribute. + */ + private boolean protect; + + /** + * Foreground color. Color.WHITE, Color.RED, etc. + */ + private Color foreColor; + + /** + * Background color. Color.WHITE, Color.RED, etc. + */ + private Color backColor; + + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Public constructor sets default values of the cell to white-on-black, + * no bold/blink/reverse/underline/protect. + * + * @see #reset() + */ + public CellAttributes() { + reset(); + } + + /** + * 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. * @@ -56,11 +118,6 @@ public class CellAttributes { this.bold = bold; } - /** - * Blink attribute. - */ - private boolean blink; - /** * Getter for blink. * @@ -79,11 +136,6 @@ public class CellAttributes { this.blink = blink; } - /** - * Reverse attribute. - */ - private boolean reverse; - /** * Getter for reverse. * @@ -102,11 +154,6 @@ public class CellAttributes { this.reverse = reverse; } - /** - * Underline attribute. - */ - private boolean underline; - /** * Getter for underline. * @@ -125,11 +172,6 @@ public class CellAttributes { this.underline = underline; } - /** - * Protected attribute. - */ - private boolean protect; - /** * Getter for protect. * @@ -148,11 +190,6 @@ public class CellAttributes { this.protect = protect; } - /** - * Foreground color. Color.WHITE, Color.RED, etc. - */ - private Color foreColor; - /** * Getter for foreColor. * @@ -171,11 +208,6 @@ public class CellAttributes { this.foreColor = foreColor; } - /** - * Background color. Color.WHITE, Color.RED, etc. - */ - private Color backColor; - /** * Getter for backColor. * @@ -208,26 +240,6 @@ public class CellAttributes { backColor = Color.BLACK; } - /** - * Public constructor sets default values of the cell to white-on-black, - * no bold/blink/reverse/underline/protect. - * - * @see #reset() - */ - public CellAttributes() { - reset(); - } - - /** - * Public constructor makes a copy from another instance. - * - * @param that another CellAttributes instance - * @see #reset() - */ - public CellAttributes(final CellAttributes that) { - setTo(that); - } - /** * Comparison check. All fields must match to return true. * diff --git a/src/jexer/bits/Color.java b/src/jexer/bits/Color.java index 0f4e6b3..120ff64 100644 --- a/src/jexer/bits/Color.java +++ b/src/jexer/bits/Color.java @@ -33,62 +33,9 @@ package jexer.bits; */ public final class Color { - /** - * The color value. Default is SGRWHITE. - */ - private int value = SGRWHITE; - - /** - * 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; - } - - /** - * Private constructor used to make the static Color instances. - * - * @param value the integer Color value - */ - private Color(final int value) { - this.value = 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; - } - } + // ------------------------------------------------------------------------ + // Constants -------------------------------------------------------------- + // ------------------------------------------------------------------------ /** * SGR black value = 0. @@ -170,6 +117,75 @@ public final class Color { */ 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). * diff --git a/src/jexer/bits/ColorTheme.java b/src/jexer/bits/ColorTheme.java index baf7685..0c6f6e4 100644 --- a/src/jexer/bits/ColorTheme.java +++ b/src/jexer/bits/ColorTheme.java @@ -46,11 +46,19 @@ import java.util.TreeMap; */ public final class ColorTheme { + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * The current theme colors. */ private SortedMap colors; + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Public constructor sets the theme to the default. */ @@ -59,6 +67,10 @@ public final class ColorTheme { setDefaultTheme(); } + // ------------------------------------------------------------------------ + // ColorTheme ------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Retrieve the CellAttributes for a named theme color. * @@ -454,6 +466,11 @@ public final class ColorTheme { 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(); @@ -479,6 +496,11 @@ public final class ColorTheme { 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(); diff --git a/src/jexer/bits/GraphicsChars.java b/src/jexer/bits/GraphicsChars.java index 3240309..b571639 100644 --- a/src/jexer/bits/GraphicsChars.java +++ b/src/jexer/bits/GraphicsChars.java @@ -34,11 +34,9 @@ package jexer.bits; */ public final class GraphicsChars { - /** - * Private constructor prevents accidental creation of this class. - */ - private GraphicsChars() { - } + // ------------------------------------------------------------------------ + // Constants -------------------------------------------------------------- + // ------------------------------------------------------------------------ /** * The CP437 to Unicode translation map. @@ -147,4 +145,15 @@ public final class GraphicsChars { 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]; + + // ------------------------------------------------------------------------ + // 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 index 3279879..edd5227 100644 --- a/src/jexer/bits/MnemonicString.java +++ b/src/jexer/bits/MnemonicString.java @@ -36,47 +36,28 @@ package jexer.bits; */ public final class MnemonicString { + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Keyboard shortcut to activate this item. */ private char shortcut; - /** - * Get the keyboard shortcut character. - * - * @return the highlighted character - */ - public char getShortcut() { - return shortcut; - } - /** * Location of the highlighted character. */ private int shortcutIdx = -1; - /** - * Get location of the highlighted character. - * - * @return location of the highlighted character - */ - public int getShortcutIdx() { - return shortcutIdx; - } - /** * The raw (uncolored) string. */ private String rawLabel; - /** - * Get the raw (uncolored) string. - * - * @return the raw (uncolored) string - */ - public String getRawLabel() { - return rawLabel; - } + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ /** * Public constructor. @@ -116,4 +97,36 @@ public final class MnemonicString { } this.rawLabel = newLabel; } + + // ------------------------------------------------------------------------ + // MnemonicString --------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Get the keyboard shortcut character. + * + * @return the highlighted character + */ + public char getShortcut() { + return shortcut; + } + + /** + * Get location of the highlighted character. + * + * @return location of the highlighted character + */ + public int getShortcutIdx() { + return shortcutIdx; + } + + /** + * Get the raw (uncolored) string. + * + * @return the raw (uncolored) string + */ + public String getRawLabel() { + return rawLabel; + } + } diff --git a/src/jexer/bits/StringJustifier.java b/src/jexer/bits/StringUtils.java similarity index 85% rename from src/jexer/bits/StringJustifier.java rename to src/jexer/bits/StringUtils.java index bbaa9cb..535720d 100644 --- a/src/jexer/bits/StringJustifier.java +++ b/src/jexer/bits/StringUtils.java @@ -32,10 +32,15 @@ import java.util.List; import java.util.LinkedList; /** - * StringJustifier contains methods to convert one or more long lines of - * strings into justified text paragraphs. + * StringUtils contains methods to: + * + * - Convert one or more long lines of strings into justified text + * paragraphs. + * + * - Unescape C0 control codes. + * */ -public final class StringJustifier { +public final class StringUtils { /** * Left-justify a string into a list of lines. @@ -237,4 +242,45 @@ public final class StringJustifier { 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(); + } + } diff --git a/src/jexer/demos/DemoTreeViewWindow.java b/src/jexer/demos/DemoTreeViewWindow.java index 7697981..a4bcbe2 100644 --- a/src/jexer/demos/DemoTreeViewWindow.java +++ b/src/jexer/demos/DemoTreeViewWindow.java @@ -32,6 +32,7 @@ import java.io.IOException; import jexer.*; import jexer.event.*; +import jexer.ttree.*; import static jexer.TCommand.*; import static jexer.TKeypress.*; @@ -43,7 +44,7 @@ public class DemoTreeViewWindow extends TWindow { /** * Hang onto my TTreeView so I can resize it with the window. */ - private TTreeView treeView; + private TTreeViewWidget treeView; /** * Public constructor. @@ -55,7 +56,7 @@ public class DemoTreeViewWindow extends TWindow { super(parent, "Tree View", 0, 0, 44, 16, TWindow.RESIZABLE); // Load the treeview with "stuff" - treeView = addTreeView(1, 1, 40, 12); + treeView = addTreeViewWidget(1, 1, 40, 12); new TDirectoryTreeItem(treeView, ".", true); statusBar = newStatusBar("Treeview demonstration"); diff --git a/src/jexer/event/TMouseEvent.java b/src/jexer/event/TMouseEvent.java index e4b4483..a6a0df2 100644 --- a/src/jexer/event/TMouseEvent.java +++ b/src/jexer/event/TMouseEvent.java @@ -35,6 +35,10 @@ package jexer.event; */ public final class TMouseEvent extends TInputEvent { + // ------------------------------------------------------------------------ + // Constants -------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * The type of event generated. */ @@ -60,11 +64,99 @@ public final class TMouseEvent extends TInputEvent { 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. * @@ -74,11 +166,6 @@ public final class TMouseEvent extends TInputEvent { return type; } - /** - * Mouse X - relative coordinates. - */ - private int x; - /** * Get x. * @@ -100,11 +187,6 @@ public final class TMouseEvent extends TInputEvent { this.x = x; } - /** - * Mouse Y - relative coordinates. - */ - private int y; - /** * Get y. * @@ -126,11 +208,6 @@ public final class TMouseEvent extends TInputEvent { this.y = y; } - /** - * Mouse X - absolute screen coordinates. - */ - private int absoluteX; - /** * Get absoluteX. * @@ -149,11 +226,6 @@ public final class TMouseEvent extends TInputEvent { this.absoluteX = absoluteX; } - /** - * Mouse Y - absolute screen coordinate. - */ - private int absoluteY; - /** * Get absoluteY. * @@ -172,11 +244,6 @@ public final class TMouseEvent extends TInputEvent { this.absoluteY = absoluteY; } - /** - * Mouse button 1 (left button). - */ - private boolean mouse1; - /** * Get mouse1. * @@ -186,11 +253,6 @@ public final class TMouseEvent extends TInputEvent { return mouse1; } - /** - * Mouse button 2 (right button). - */ - private boolean mouse2; - /** * Get mouse2. * @@ -200,11 +262,6 @@ public final class TMouseEvent extends TInputEvent { return mouse2; } - /** - * Mouse button 3 (middle button). - */ - private boolean mouse3; - /** * Get mouse3. * @@ -214,11 +271,6 @@ public final class TMouseEvent extends TInputEvent { return mouse3; } - /** - * Mouse wheel UP (button 4). - */ - private boolean mouseWheelUp; - /** * Get mouseWheelUp. * @@ -228,11 +280,6 @@ public final class TMouseEvent extends TInputEvent { return mouseWheelUp; } - /** - * Mouse wheel DOWN (button 5). - */ - private boolean mouseWheelDown; - /** * Get mouseWheelDown. * @@ -242,37 +289,6 @@ public final class TMouseEvent extends TInputEvent { return mouseWheelDown; } - /** - * 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; - } - /** * Create a duplicate instance. * diff --git a/src/jexer/event/TResizeEvent.java b/src/jexer/event/TResizeEvent.java index 8b2b367..4e15121 100644 --- a/src/jexer/event/TResizeEvent.java +++ b/src/jexer/event/TResizeEvent.java @@ -33,6 +33,10 @@ package jexer.event; */ public final class TResizeEvent extends TInputEvent { + // ------------------------------------------------------------------------ + // Constants -------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Resize events can be generated for either a total screen resize or a * widget/window resize. @@ -49,11 +53,46 @@ public final class TResizeEvent extends TInputEvent { 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. * @@ -63,11 +102,6 @@ public final class TResizeEvent extends TInputEvent { return type; } - /** - * New width. - */ - private int width; - /** * Get the new width. * @@ -77,11 +111,6 @@ public final class TResizeEvent extends TInputEvent { return width; } - /** - * New height. - */ - private int height; - /** * Get the new height. * @@ -91,19 +120,6 @@ public final class TResizeEvent extends TInputEvent { return height; } - /** - * 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; - } - /** * Make human-readable description of this TResizeEvent. * diff --git a/src/jexer/menu/TMenu.java b/src/jexer/menu/TMenu.java index 667673b..be4cf5d 100644 --- a/src/jexer/menu/TMenu.java +++ b/src/jexer/menu/TMenu.java @@ -51,47 +51,9 @@ public final class TMenu extends TWindow { */ private static final ResourceBundle i18n = ResourceBundle.getBundle(TMenu.class.getName()); - /** - * 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; - - /** - * 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; - } - - /** - * The shortcut and title. - */ - private MnemonicString mnemonic; - - /** - * Get the mnemonic string. - * - * @return the full mnemonic string - */ - public MnemonicString getMnemonic() { - return mnemonic; - } + // ------------------------------------------------------------------------ + // Constants -------------------------------------------------------------- + // ------------------------------------------------------------------------ // Reserved menu item IDs public static final int MID_UNUSED = -1; @@ -108,15 +70,21 @@ public final class TMenu extends TWindow { public static final int MID_PASTE = 12; public static final int MID_CLEAR = 13; + // Search menu + public static final int MID_FIND = 20; + public static final int MID_REPLACE = 21; + public static final int MID_SEARCH_AGAIN = 22; + public static final int MID_GOTO_LINE = 23; + // Window menu - public static final int MID_TILE = 20; - public static final int MID_CASCADE = 21; - public static final int MID_CLOSE_ALL = 22; - public static final int MID_WINDOW_MOVE = 23; - public static final int MID_WINDOW_ZOOM = 24; - public static final int MID_WINDOW_NEXT = 25; - public static final int MID_WINDOW_PREVIOUS = 26; - public static final int MID_WINDOW_CLOSE = 27; + public static final int MID_TILE = 30; + public static final int MID_CASCADE = 31; + public static final int MID_CLOSE_ALL = 32; + public static final int MID_WINDOW_MOVE = 33; + public static final int MID_WINDOW_ZOOM = 34; + public static final int MID_WINDOW_NEXT = 35; + public static final int MID_WINDOW_PREVIOUS = 36; + public static final int MID_WINDOW_CLOSE = 37; // Help menu public static final int MID_HELP_CONTENTS = 40; @@ -130,6 +98,29 @@ public final class TMenu extends TWindow { // Other public static final int MID_REPAINT = 50; + // ------------------------------------------------------------------------ + // 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. * @@ -158,46 +149,9 @@ public final class TMenu extends TWindow { setActive(false); } - /** - * 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 - getScreen().drawBoxShadow(0, 0, getWidth(), getHeight()); - } + // ------------------------------------------------------------------------ + // Event handlers --------------------------------------------------------- + // ------------------------------------------------------------------------ /** * Handle mouse button presses. @@ -345,6 +299,82 @@ public final class TMenu extends TWindow { } } + // ------------------------------------------------------------------------ + // 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 + getScreen().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. * @@ -400,11 +430,27 @@ public final class TMenu extends TWindow { 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); @@ -426,6 +472,18 @@ public final class TMenu extends TWindow { * @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); @@ -465,6 +523,20 @@ public final class TMenu extends TWindow { // 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"); + break; + case MID_GOTO_LINE: + label = i18n.getString("menuGotoLine"); + key = kbCtrlL; + break; + case MID_TILE: label = i18n.getString("menuWindowTile"); break; @@ -528,7 +600,7 @@ public final class TMenu extends TWindow { throw new IllegalArgumentException("Invalid menu ID: " + id); } - return addItemInternal(id, label, key); + return addItemInternal(id, label, key, enabled); } /** diff --git a/src/jexer/menu/TMenu.properties b/src/jexer/menu/TMenu.properties index 000df91..d42157c 100644 --- a/src/jexer/menu/TMenu.properties +++ b/src/jexer/menu/TMenu.properties @@ -5,6 +5,10 @@ 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 diff --git a/src/jexer/menu/TMenuItem.java b/src/jexer/menu/TMenuItem.java index 9ffc1ea..63a5355 100644 --- a/src/jexer/menu/TMenuItem.java +++ b/src/jexer/menu/TMenuItem.java @@ -43,6 +43,10 @@ import static jexer.TKeypress.*; */ public class TMenuItem extends TWidget { + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Label for this menu item. */ @@ -54,29 +58,11 @@ public class TMenuItem extends TWidget { */ private int id = TMenu.MID_UNUSED; - /** - * Get the menu item ID. - * - * @return the id - */ - public final int getId() { - return id; - } - /** * When true, this item can be checked or unchecked. */ private boolean checkable = false; - /** - * Set checkable flag. - * - * @param checkable if true, this menu item can be checked/unchecked - */ - public final void setCheckable(final boolean checkable) { - this.checkable = checkable; - } - /** * When true, this item is checked. */ @@ -93,40 +79,9 @@ public class TMenuItem extends TWidget { */ private MnemonicString mnemonic; - /** - * 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 = (label.length() + 4 + key.toString().length() + 2); - if (newWidth > getWidth()) { - setWidth(newWidth); - } - } - } + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ /** * Package private constructor. @@ -190,6 +145,10 @@ public class TMenuItem extends TWidget { } + // ------------------------------------------------------------------------ + // Event handlers --------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Returns true if the mouse is currently on the menu item. * @@ -206,6 +165,39 @@ public class TMenuItem extends TWidget { 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. */ @@ -249,44 +241,73 @@ public class TMenuItem extends TWidget { } + // ------------------------------------------------------------------------ + // TMenuItem -------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** - * Dispatch event(s) due to selection or click. + * Get the menu item ID. + * + * @return the id */ - public void dispatch() { - assert (isEnabled()); + public final int getId() { + return id; + } - getApplication().postMenuEvent(new TMenuEvent(id)); - if (checkable) { - checked = !checked; - } + /** + * Set checkable flag. + * + * @param checkable if true, this menu item can be checked/unchecked + */ + public final void setCheckable(final boolean checkable) { + this.checkable = checkable; } /** - * Handle mouse button releases. + * Get the mnemonic string for this menu item. * - * @param mouse mouse button release event + * @return mnemonic string */ - @Override - public void onMouseUp(final TMouseEvent mouse) { - if ((mouseOnMenuItem(mouse)) && (mouse.isMouse1())) { - dispatch(); - return; - } + public final MnemonicString getMnemonic() { + return mnemonic; } /** - * Handle keystrokes. + * Get a global accelerator key for this menu item. * - * @param keypress keystroke event + * @return global keyboard accelerator, or null if no key is associated + * with this item */ - @Override - public void onKeypress(final TKeypressEvent keypress) { - if (keypress.equals(kbEnter)) { - dispatch(); - return; + 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 = (label.length() + 4 + key.toString().length() + 2); + if (newWidth > getWidth()) { + setWidth(newWidth); + } } + } - // Pass to parent for the things we don't care about. - super.onKeypress(keypress); + /** + * 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 index 9a9fcfc..01164a5 100644 --- a/src/jexer/menu/TMenuSeparator.java +++ b/src/jexer/menu/TMenuSeparator.java @@ -36,6 +36,10 @@ import jexer.bits.GraphicsChars; */ public final class TMenuSeparator extends TMenuItem { + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Package private constructor. * @@ -50,6 +54,10 @@ public final class TMenuSeparator extends TMenuItem { setWidth(parent.getWidth() - 2); } + // ------------------------------------------------------------------------ + // TMenuItem -------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Draw a menu separator. */ diff --git a/src/jexer/menu/TSubMenu.java b/src/jexer/menu/TSubMenu.java index 7b5f80c..88094da 100644 --- a/src/jexer/menu/TSubMenu.java +++ b/src/jexer/menu/TSubMenu.java @@ -40,11 +40,19 @@ import static jexer.TKeypress.*; */ public final class TSubMenu extends TMenuItem { + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * The menu window. Note package private access. */ TMenu menu; + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Package private constructor. * @@ -67,28 +75,9 @@ public final class TSubMenu extends TMenuItem { this.menu.isSubMenu = true; } - /** - * 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 - getScreen().putCharXY(getWidth() - 2, 0, GraphicsChars.CP437[0x10], - menuColor); - } + // ------------------------------------------------------------------------ + // Event handlers --------------------------------------------------------- + // ------------------------------------------------------------------------ /** * Handle keystrokes. @@ -151,6 +140,33 @@ public final class TSubMenu extends TMenuItem { } } + // ------------------------------------------------------------------------ + // 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 + getScreen().putCharXY(getWidth() - 2, 0, GraphicsChars.CP437[0x10], + menuColor); + } + /** * Override dispatch() to do nothing. */ @@ -179,6 +195,10 @@ public final class TSubMenu extends TMenuItem { return this; } + // ------------------------------------------------------------------------ + // TSubMenu --------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Convenience function to add a custom menu item. * @@ -233,5 +253,4 @@ public final class TSubMenu extends TMenuItem { return menu.addSubMenu(title); } - } diff --git a/src/jexer/net/TelnetInputStream.java b/src/jexer/net/TelnetInputStream.java index ea30171..9ae7747 100644 --- a/src/jexer/net/TelnetInputStream.java +++ b/src/jexer/net/TelnetInputStream.java @@ -43,6 +43,14 @@ import static jexer.net.TelnetSocket.*; public final class TelnetInputStream extends InputStream implements SessionInfo { + // ------------------------------------------------------------------------ + // Constants -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * The root TelnetSocket that has my telnet protocol state. */ @@ -76,6 +84,61 @@ public final class TelnetInputStream extends InputStream */ 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. * @@ -97,27 +160,9 @@ public final class TelnetInputStream extends InputStream subnegBuffer = new ArrayList(); } - // SessionInfo interface -------------------------------------------------- - - /** - * 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; + // ------------------------------------------------------------------------ + // SessionInfo ------------------------------------------------------------ + // ------------------------------------------------------------------------ /** * Username getter. @@ -180,7 +225,9 @@ public final class TelnetInputStream extends InputStream // NOP } - // InputStream interface -------------------------------------------------- + // ------------------------------------------------------------------------ + // InputStream ------------------------------------------------------------ + // ------------------------------------------------------------------------ /** * Returns an estimate of the number of bytes that can be read (or @@ -371,39 +418,9 @@ public final class TelnetInputStream extends InputStream return n; } - // Telnet protocol -------------------------------------------------------- - - - /** - * 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; + // ------------------------------------------------------------------------ + // TelnetInputStream ------------------------------------------------------ + // ------------------------------------------------------------------------ /** * For debugging, return a descriptive string for this telnet option. @@ -1348,5 +1365,4 @@ public final class TelnetInputStream extends InputStream return bufN; } - } diff --git a/src/jexer/net/TelnetOutputStream.java b/src/jexer/net/TelnetOutputStream.java index 3520a59..338de2c 100644 --- a/src/jexer/net/TelnetOutputStream.java +++ b/src/jexer/net/TelnetOutputStream.java @@ -38,6 +38,10 @@ import static jexer.net.TelnetSocket.*; */ public final class TelnetOutputStream extends OutputStream { + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * The root TelnetSocket that has my telnet protocol state. */ @@ -48,6 +52,15 @@ public final class TelnetOutputStream extends 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. * @@ -59,7 +72,9 @@ public final class TelnetOutputStream extends OutputStream { this.output = output; } - // OutputStream interface ------------------------------------------------- + // ------------------------------------------------------------------------ + // OutputStrem ------------------------------------------------------------ + // ------------------------------------------------------------------------ /** * Closes this output stream and releases any system resources associated @@ -135,6 +150,10 @@ public final class TelnetOutputStream extends OutputStream { writeImpl(bytes, 0, 1); } + // ------------------------------------------------------------------------ + // TelnetOutputStrem ------------------------------------------------------ + // ------------------------------------------------------------------------ + /** * Writes b.length bytes from the specified byte array to this output * stream. Note package private access. @@ -146,13 +165,6 @@ public final class TelnetOutputStream extends OutputStream { output.write(b, 0, b.length); } - // Telnet protocol -------------------------------------------------------- - - /** - * When true, the last byte the caller passed to write() was a CR. - */ - private boolean writeCR = false; - /** * Writes len bytes from the specified byte array starting at offset off * to this output stream. diff --git a/src/jexer/net/TelnetServerSocket.java b/src/jexer/net/TelnetServerSocket.java index 923ba29..7523192 100644 --- a/src/jexer/net/TelnetServerSocket.java +++ b/src/jexer/net/TelnetServerSocket.java @@ -39,7 +39,14 @@ import java.net.SocketException; */ public final class TelnetServerSocket extends ServerSocket { - // ServerSocket interface ------------------------------------------------- + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ /** * Creates an unbound server socket. @@ -94,6 +101,10 @@ public final class TelnetServerSocket extends ServerSocket { 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. diff --git a/src/jexer/net/TelnetSocket.java b/src/jexer/net/TelnetSocket.java index 74ad72a..3166388 100644 --- a/src/jexer/net/TelnetSocket.java +++ b/src/jexer/net/TelnetSocket.java @@ -40,15 +40,9 @@ import java.net.Socket; */ public final class TelnetSocket extends Socket { - /** - * The telnet-aware socket InputStream. - */ - private TelnetInputStream input; - - /** - * The telnet-aware socket OutputStream. - */ - private TelnetOutputStream output; + // ------------------------------------------------------------------------ + // Constants -------------------------------------------------------------- + // ------------------------------------------------------------------------ // Telnet protocol special characters. Note package private access. static final int TELNET_SE = 240; @@ -71,6 +65,21 @@ public final class TelnetSocket extends Socket { 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()). */ @@ -126,14 +135,9 @@ public final class TelnetSocket extends Socket { */ String terminalSpeed = ""; - /** - * See if telnet server/client is in ASCII mode. - * - * @return if true, this connection is in ASCII mode - */ - public boolean isAscii() { - return (!binaryMode); - } + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ /** * Creates a Socket that knows the telnet protocol. Note package private @@ -145,7 +149,9 @@ public final class TelnetSocket extends Socket { super(); } - // Socket interface ------------------------------------------------------- + // ------------------------------------------------------------------------ + // Socket ----------------------------------------------------------------- + // ------------------------------------------------------------------------ /** * Returns an input stream for this socket. @@ -181,4 +187,17 @@ public final class TelnetSocket extends Socket { 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/tterminal/DECCharacterSets.java b/src/jexer/tterminal/DECCharacterSets.java index 4cc8757..5110db0 100644 --- a/src/jexer/tterminal/DECCharacterSets.java +++ b/src/jexer/tterminal/DECCharacterSets.java @@ -34,11 +34,9 @@ package jexer.tterminal; */ public final class DECCharacterSets { - /** - * Private constructor prevents accidental creation of this class. - */ - private DECCharacterSets() { - } + // ------------------------------------------------------------------------ + // Constants -------------------------------------------------------------- + // ------------------------------------------------------------------------ /** * US - Normal "international" (ASCII). @@ -370,4 +368,14 @@ public final class DECCharacterSets { 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 index 1573948..8eff2de 100644 --- a/src/jexer/tterminal/DisplayLine.java +++ b/src/jexer/tterminal/DisplayLine.java @@ -35,16 +35,68 @@ import jexer.bits.CellAttributes; * This represents a single line of the display buffer. */ public final 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 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(); + chars[i].setTo(attr); + } + } + + // ------------------------------------------------------------------------ + // DisplayLine ------------------------------------------------------------ + // ------------------------------------------------------------------------ + /** * Get the Cell at a specific column. * @@ -64,11 +116,6 @@ public final class DisplayLine { return chars.length; } - /** - * Double-width line flag. - */ - private boolean doubleWidth = false; - /** * Get double width flag. * @@ -87,17 +134,6 @@ public final class DisplayLine { this.doubleWidth = doubleWidth; } - /** - * Double height line flag. Valid values are: - * - *

-     *   0 = single height
-     *   1 = top half double height
-     *   2 = bottom half double height
-     * 
- */ - private int doubleHeight = 0; - /** * Get double height flag. * @@ -116,12 +152,6 @@ public final class DisplayLine { this.doubleHeight = doubleHeight; } - /** - * 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; - /** * Get reverse video flag. * @@ -140,19 +170,6 @@ public final class DisplayLine { this.reverseColor = 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(); - chars[i].setTo(attr); - } - } - /** * Insert a character at the specified position. * diff --git a/src/jexer/tterminal/ECMA48.java b/src/jexer/tterminal/ECMA48.java index caa295a..7a37a95 100644 --- a/src/jexer/tterminal/ECMA48.java +++ b/src/jexer/tterminal/ECMA48.java @@ -90,6 +90,10 @@ import static jexer.TKeypress.*; */ public class ECMA48 implements Runnable { + // ------------------------------------------------------------------------ + // Constants -------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * The emulator can emulate several kinds of terminals. */ @@ -116,194 +120,115 @@ public class ECMA48 implements Runnable { } /** - * Return the proper primary Device Attributes string. - * - * @return string to send to remote side that is appropriate for the - * this.type + * Parser character scan states. */ - 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;6c"; - } - // "I am a VT220" - 8 bit version - return "\u009b?62;1;6c"; - default: - throw new IllegalArgumentException("Invalid device type: " + type); - } + 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, + SOSPMAPC_STRING, + OSC_STRING, + VT52_DIRECT_CURSOR_ADDRESS } /** - * Return the proper TERM environment variable for this device type. - * - * @param deviceType DeviceType.VT100, DeviceType, XTERM, etc. - * @return "vt100", "xterm", etc. + * 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. */ - 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); - } + private enum KeypadMode { + Application, + Numeric } /** - * 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. + * Arrow keys can emit three different sequences (DECCKM or VT52 + * submode). */ - 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); - } + private enum ArrowKeyMode { + VT52, + ANSI, + VT100 } /** - * Write a string directly to the remote side. - * - * @param str string to send + * Available character sets for GL, GR, G0, G1, G2, G3. */ - public void writeRemote(final String str) { - if (stopReaderThread) { - // Reader hit EOF, bail out now. - close(); - return; - } - - // System.err.printf("writeRemote() '%s'\n", str); + 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 + } - 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); - } + /** + * Single-shift states used by the C1 control characters SS2 (0x8E) and + * SS3 (0x8F). + */ + private enum Singleshift { + NONE, + SS2, + SS3 } /** - * Close the input and output streams and stop the reader thread. Note - * that it is safe to call this multiple times. + * VT220+ lockshift states. */ - public final void close() { + private enum LockshiftMode { + NONE, + G1_GR, + G2_GR, + G2_GL, + G3_GR, + G3_GL + } - // 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) { - e.printStackTrace(); - } - } + /** + * XTERM mouse reporting protocols. + */ + private enum MouseProtocol { + OFF, + X10, + NORMAL, + BUTTONEVENT, + ANYEVENT + } - // 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); - } + /** + * XTERM mouse reporting encodings. + */ + private enum MouseEncoding { + X10, + UTF8, + SGR } + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * The enclosing listening object. */ @@ -320,58 +245,20 @@ public class ECMA48 implements Runnable { private Thread readerThread = null; /** - * 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); - } - - /** - * The type of emulator to be. + * The type of emulator to be. */ private DeviceType type = DeviceType.VT102; - /** - * 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); - } - /** * The scrollback buffer characters + attributes. */ private volatile List scrollback; - /** - * Get the scrollback buffer. - * - * @return the scrollback buffer - */ - public final List getScrollbackBuffer() { - return scrollback; - } - /** * The raw display buffer characters + attributes. */ private volatile List display; - /** - * Get the display buffer. - * - * @return the display buffer - */ - public final List getDisplayBuffer() { - return display; - } - /** * The terminal's input. For type == XTERM, this is an InputStreamReader * with UTF-8 encoding. @@ -394,122 +281,16 @@ public class ECMA48 implements Runnable { */ private OutputStream outputStream; - /** - * Parser character scan states. - */ - enum ScanState { - GROUND, - ESCAPE, - ESCAPE_INTERMEDIATE, - CSI_ENTRY, - CSI_PARAM, - CSI_INTERMEDIATE, - CSI_IGNORE, - DCS_ENTRY, - DCS_INTERMEDIATE, - DCS_PARAM, - DCS_PASSTHROUGH, - DCS_IGNORE, - SOSPMAPC_STRING, - OSC_STRING, - VT52_DIRECT_CURSOR_ADDRESS - } - /** * Current scanning state. */ private ScanState scanState; - /** - * 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. - */ - private enum MouseProtocol { - OFF, - X10, - NORMAL, - BUTTONEVENT, - ANYEVENT - } - /** * Which mouse protocol is active. */ private MouseProtocol mouseProtocol = MouseProtocol.OFF; - /** - * XTERM mouse reporting encodings. - */ - private enum MouseEncoding { - X10, - UTF8, - SGR - } - /** * Which mouse encoding is active. */ @@ -521,77 +302,12 @@ public class ECMA48 implements Runnable { */ private int width; - /** - * 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 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; - } - } - /** * Physical display height. We start at 80x24, but the user can resize * us bigger/smaller. */ private int height; - /** - * 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 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)); - } - } - /** * Top margin of the scrolling region. */ @@ -641,32 +357,12 @@ public class ECMA48 implements Runnable { */ private boolean cursorVisible = true; - /** - * Get visible cursor flag. - * - * @return if true, the cursor is visible - */ - public final boolean isCursorVisible() { - return cursorVisible; - } - /** * 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 = ""; - /** - * 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; - } - /** * Parameter characters being collected. */ @@ -722,15 +418,6 @@ public class ECMA48 implements Runnable { */ private boolean columns132 = false; - /** - * Get 132 columns value. - * - * @return if true, the terminal is in 132 column mode - */ - public final boolean isColumns132() { - return columns132; - } - /** * true = reverse video. Set by DECSCNM. */ @@ -741,6 +428,16 @@ public class ECMA48 implements Runnable { */ private boolean fullDuplex = true; + /** + * The current terminal state. + */ + private SaveableState currentState; + + /** + * The last saved terminal state. + */ + private SaveableState savedState; + /** * DECSC/DECRC save/restore a subset of the total state. This class * encapsulates those specific flags/modes. @@ -846,23 +543,520 @@ public class ECMA48 implements Runnable { this.lineWrap = that.lineWrap; } - /** - * Public constructor. - */ - public SaveableState() { - reset(); + /** + * 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 LinkedList(); + display = new LinkedList(); + + 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[128]; + } else { + readBuffer = new byte[128]; + } + + while (!done && !stopReaderThread) { + 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(2); + } 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) { + for (int i = 0; i < rc; i++) { + int ch = 0; + if (utf8) { + ch = readBufferUTF8[i]; + } else { + ch = readBuffer[i]; + } + + consume((char)ch); + } + } + // 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) { + e.printStackTrace(); + done = true; + } + + } // 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 ----------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * 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;6c"; + } + // "I am a VT220" - 8 bit version + return "\u009b?62;1;6c"; + 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) { + e.printStackTrace(); + } + } + + // 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 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 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 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)); } } /** - * The current terminal state. + * Get visible cursor flag. + * + * @return if true, the cursor is visible */ - private SaveableState currentState; + public final boolean isCursorVisible() { + return cursorVisible; + } /** - * The last saved terminal state. + * 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 */ - private SaveableState savedState; + 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. @@ -932,61 +1126,6 @@ public class ECMA48 implements Runnable { toGround(); } - /** - * 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 LinkedList(); - display = new LinkedList(); - - 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(); - } - /** * Append a new line to the bottom of the display, adding lines off the * top to the scrollback buffer. @@ -6015,123 +6154,4 @@ public class ECMA48 implements Runnable { return currentState.cursorY; } - /** - * 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[128]; - } else { - readBuffer = new byte[128]; - } - - while (!done && !stopReaderThread) { - 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(2); - } 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) { - for (int i = 0; i < rc; i++) { - int ch = 0; - if (utf8) { - ch = readBufferUTF8[i]; - } else { - ch = readBuffer[i]; - } - - consume((char)ch); - } - } - // 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) { - e.printStackTrace(); - done = true; - } - - } // 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(); - } - } diff --git a/src/jexer/TDirectoryTreeItem.java b/src/jexer/ttree/TDirectoryTreeItem.java similarity index 80% rename from src/jexer/TDirectoryTreeItem.java rename to src/jexer/ttree/TDirectoryTreeItem.java index 6d5d018..c260d7f 100644 --- a/src/jexer/TDirectoryTreeItem.java +++ b/src/jexer/ttree/TDirectoryTreeItem.java @@ -26,7 +26,7 @@ * @author Kevin Lamonte [kevin.lamonte@gmail.com] * @version 1 */ -package jexer; +package jexer.ttree; import java.io.File; import java.io.IOException; @@ -34,116 +34,41 @@ import java.util.Collections; import java.util.List; import java.util.LinkedList; +import jexer.TWidget; +import jexer.ttree.TTreeViewWidget; + /** * 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; /** - * Get the File corresponding to this list item. - * - * @return the File + * The TTreeViewWidget containing this directory tree. */ - public final File getFile() { - return file; - } + private TTreeViewWidget treeViewWidget; - /** - * 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()) { - getTreeView().reflowData(); - return; - } - - for (File f: file.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(getTreeView(), - f.getCanonicalPath(), false, false); - - item.level = this.level + 1; - getChildren().add(item); - } catch (IOException e) { - continue; - } - } - Collections.sort(getChildren()); - - getTreeView().reflowData(); - } - - /** - * Add a child item. This method should never be used, it will throw an - * IllegalArgumentException every time. - * - * @param text text for this item - * @param expanded if true, have it expanded immediately - * @return the new item - * @throws IllegalArgumentException if this function is called - */ - @Override - public final TTreeItem addChild(final String text, - final boolean expanded) throws IllegalArgumentException { - - throw new IllegalArgumentException("Do not call addChild(), use onExpand() instead"); - } + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ /** * Public constructor. * - * @param view root TTreeView - * @param text text for this item - * @throws IOException if a java.io operation throws - */ - public TDirectoryTreeItem(final TTreeView view, - final String text) throws IOException { - - this(view, text, false, true); - } - - /** - * Public constructor. - * - * @param view root TTreeView + * @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 TTreeView view, final String text, + public TDirectoryTreeItem(final TTreeViewWidget view, final String text, final boolean expanded) throws IOException { this(view, text, expanded, true); @@ -152,17 +77,19 @@ public class TDirectoryTreeItem extends TTreeItem { /** * Public constructor. * - * @param view root TTreeView + * @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 TTreeView view, final String text, + public TDirectoryTreeItem(final TTreeViewWidget view, final String text, final boolean expanded, final boolean openParents) throws IOException { - super(view, text, false); + super(view.getTreeView(), text, false); + + this.treeViewWidget = view; List parentFiles = new LinkedList(); boolean oldExpanded = expanded; @@ -209,9 +136,74 @@ public class TDirectoryTreeItem extends TTreeItem { } } unselect(); - getTreeView().setSelected(childFile); + getTreeView().setSelected(childFile, true); setExpanded(oldExpanded); } - getTreeView().reflowData(); + + 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; + } + + for (File f: file.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/TTreeItem.java b/src/jexer/ttree/TTreeItem.java similarity index 76% rename from src/jexer/TTreeItem.java rename to src/jexer/ttree/TTreeItem.java index 7de5e12..901ce85 100644 --- a/src/jexer/TTreeItem.java +++ b/src/jexer/ttree/TTreeItem.java @@ -26,11 +26,12 @@ * @author Kevin Lamonte [kevin.lamonte@gmail.com] * @version 1 */ -package jexer; +package jexer.ttree; import java.util.ArrayList; import java.util.List; +import jexer.TWidget; import jexer.bits.CellAttributes; import jexer.bits.GraphicsChars; import jexer.event.TKeypressEvent; @@ -42,164 +43,55 @@ import static jexer.TKeypress.*; */ 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; - /** - * Get the parent TTreeView. - * - * @return the parent TTreeView - */ - public final TTreeView getTreeView() { - return view; - } - /** * Displayable text for this item. */ private String text; - /** - * 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; - } - /** * If true, this item is expanded in the tree view. */ private boolean expanded = true; - /** - * 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) { - this.expanded = expanded; - } - /** * If true, this item can be expanded in the tree view. */ private boolean expandable = false; - /** - * 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) { - this.expandable = expandable; - } - /** * The vertical bars and such along the left side. */ private String prefix = ""; /** - * Get the vertical bars and such along the left side. - * - * @return the vertical bars and such along the left side + * Tree level. */ - public final String getPrefix() { - return prefix; - } - - /** - * Whether or not this item is last in its parent's list of children. - */ - private boolean last = false; - - /** - * Tree level. Note package private access. - */ - int level = 0; - - /** - * If true, this item will not be drawn. - */ - private boolean invisible = false; - - /** - * Set invisible value. - * - * @param invisible new value - */ - public final void setInvisible(final boolean invisible) { - this.invisible = invisible; - } + protected int level = 0; /** * True means selected. */ private boolean selected = false; - /** - * 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; - } - /** * True means select-able. */ private boolean selectable = true; /** - * Set selectable value. - * - * @param selectable new value + * Whether or not this item is last in its parent's list of children. */ - public final void setSelectable(final boolean selectable) { - this.selectable = selectable; - } + private boolean last = false; /** * Pointer to the previous keyboard-navigable item (kbUp). Note package @@ -213,6 +105,10 @@ public class TTreeItem extends TWidget { */ TTreeItem keyboardNext = null; + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * Public constructor. * @@ -224,107 +120,21 @@ public class TTreeItem extends TWidget { 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, true); - } - - view.reflowData(); - } - - /** - * Add a child item. - * - * @param text text for this item - * @return the new child item - */ - public TTreeItem addChild(final String text) { - return addChild(text, true); - } - - /** - * Add a child item. - * - * @param text text for this item - * @param expanded if true, have it expanded immediately - * @return the new child item - */ - public TTreeItem addChild(final String text, final boolean expanded) { - TTreeItem item = new TTreeItem(view, text, expanded); - item.level = this.level + 1; - getChildren().add(item); - view.reflowData(); - return item; - } - - /** - * 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; + view.setTreeRoot(this); + } else { + view.alignTree(); } - return prefix.length() + 3; } - /** - * Recursively unselect my or my children. - */ - public void unselect() { - if (selected == true) { - selected = false; - view.setSelected(null); - } - for (TWidget widget: getChildren()) { - if (widget instanceof TTreeItem) { - TTreeItem item = (TTreeItem) widget; - item.unselect(); - } - } - } + // ------------------------------------------------------------------------ + // Event handlers --------------------------------------------------------- + // ------------------------------------------------------------------------ /** * Handle mouse release events. @@ -333,9 +143,13 @@ public class TTreeItem extends TWidget { */ @Override public void onMouseUp(final TMouseEvent mouse) { - if ((mouse.getX() == (getExpanderX() - view.getHorizontalValue())) + 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; @@ -343,16 +157,18 @@ public class TTreeItem extends TWidget { // 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) { - view.setSelected(this); + // Do the action associated with this item. + view.setSelected(this, false); view.dispatch(); } - - // Update the screen after any thing has expanded/contracted - view.reflowData(); } /** @@ -377,6 +193,10 @@ public class TTreeItem extends TWidget { || keypress.equals(kbRight) || keypress.equals(kbSpace) ) { + if (level == 0) { + // Root node can't switch. + return; + } if (selectable) { // Flip expanded flag expanded = !expanded; @@ -384,26 +204,33 @@ public class TTreeItem extends TWidget { // Unselect children that became invisible unselect(); } - view.setSelected(this); + 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 (invisible) { + if ((getY() < 0) || (getY() > getParent().getHeight() - 1)) { return; } - int offset = -view.getHorizontalValue(); + int offset = -view.getLeftColumn(); CellAttributes color = getTheme().getColor("ttreeview"); CellAttributes textColor = getTheme().getColor("ttreeview"); @@ -413,6 +240,7 @@ public class TTreeItem extends TWidget { if (!getParent().isAbsoluteActive()) { color = getTheme().getColor("ttreeview.inactive"); textColor = getTheme().getColor("ttreeview.inactive"); + selectedColor = getTheme().getColor("ttreeview.selected.inactive"); } if (!selectable) { @@ -432,6 +260,8 @@ public class TTreeItem extends TWidget { line += GraphicsChars.CP437[0xC4]; if (expandable) { line += "[ ] "; + } else { + line += " "; } } getScreen().putStringXY(offset, 0, line, color); @@ -452,4 +282,204 @@ public class TTreeItem extends TWidget { } } + // ------------------------------------------------------------------------ + // 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 + text.length(); + for (TWidget widget: getChildren()) { + TTreeItem item = (TTreeItem) widget; + int n = item.prefix.length() + 4 + item.text.length(); + 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..acc8924 --- /dev/null +++ b/src/jexer/ttree/TTreeView.java @@ -0,0 +1,329 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2017 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN 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(); + } + } + + /** + * 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/TTreeView.java b/src/jexer/ttree/TTreeViewWidget.java similarity index 58% rename from src/jexer/TTreeView.java rename to src/jexer/ttree/TTreeViewWidget.java index b9b05bf..adb9a5d 100644 --- a/src/jexer/TTreeView.java +++ b/src/jexer/ttree/TTreeViewWidget.java @@ -26,49 +26,31 @@ * @author Kevin Lamonte [kevin.lamonte@gmail.com] * @version 1 */ -package jexer; - +package jexer.ttree; + +import jexer.TAction; +import jexer.THScroller; +import jexer.TKeypress; +import jexer.TScrollableWidget; +import jexer.TVScroller; +import jexer.TWidget; import jexer.event.TKeypressEvent; import jexer.event.TMouseEvent; import static jexer.TKeypress.*; /** - * TTreeView implements a simple tree view. + * TTreeViewWidget wraps a tree view with horizontal and vertical scrollbars. */ -public class TTreeView extends TScrollableWidget { - - /** - * Root of the tree. - */ - private TTreeItem treeRoot; - - /** - * 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; - } +public class TTreeViewWidget extends TScrollableWidget { - /** - * Maximum width of a single line. - */ - private int maxLineWidth; + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ /** - * Only one of my children can be selected. + * The TTreeView */ - private TTreeItem selectedItem = null; + private TTreeView treeView; /** * If true, move the window to put the selected item in view. This @@ -77,22 +59,13 @@ public class TTreeView extends TScrollableWidget { private boolean centerWindow = false; /** - * The action to perform when the user selects an item. - */ - private TAction action = null; - - /** - * Set treeRoot. - * - * @param treeRoot ultimate root of tree - * @param centerWindow if true, move the window to put the root in view + * Maximum width of a single line. */ - public void setTreeRoot(final TTreeItem treeRoot, - final boolean centerWindow) { + private int maxLineWidth; - this.treeRoot = treeRoot; - this.centerWindow = centerWindow; - } + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ /** * Public constructor. @@ -103,7 +76,7 @@ public class TTreeView extends TScrollableWidget { * @param width width of tree view * @param height height of tree view */ - public TTreeView(final TWidget parent, final int x, final int y, + public TTreeViewWidget(final TWidget parent, final int x, final int y, final int width, final int height) { this(parent, x, y, width, height, null); @@ -119,174 +92,22 @@ public class TTreeView extends TScrollableWidget { * @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, + 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); - this.action = action; + + 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); - } - - /** - * 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 - */ - public void setSelected(final TTreeItem item) { - if (item != null) { - item.setSelected(true); - } - if ((selectedItem != null) && (selectedItem != item)) { - selectedItem.setSelected(false); - } - selectedItem = item; - } - - /** - * Perform user selection action. - */ - public void dispatch() { - if (action != null) { - action.DO(); - } - } - /** - * Resize text and scrollbars for a new width/height. - */ - @Override - public void reflowData() { - int selectedRow = 0; - boolean foundSelectedRow = false; - - if (treeRoot == null) { - return; - } - - // Make each child invisible/inactive to start, expandTree() will - // reactivate the visible ones. - for (TWidget widget: getChildren()) { - if (widget instanceof TTreeItem) { - TTreeItem item = (TTreeItem) widget; - item.setInvisible(true); - item.setEnabled(false); - item.keyboardPrevious = null; - item.keyboardNext = null; - } - } - - // Expand the tree into a linear list - getChildren().clear(); - getChildren().addAll(treeRoot.expandTree("", true)); - - // Locate the selected row and maximum line width - for (TWidget widget: getChildren()) { - TTreeItem item = (TTreeItem) widget; - - if (item == selectedItem) { - foundSelectedRow = true; - } - if (!foundSelectedRow) { - selectedRow++; - } - - int lineWidth = item.getText().length() - + item.getPrefix().length() + 4; - if (lineWidth > maxLineWidth) { - maxLineWidth = lineWidth; - } - } - - if ((centerWindow) && (foundSelectedRow)) { - if ((selectedRow < getVerticalValue()) - || (selectedRow > getVerticalValue() + getHeight() - 2) - ) { - setVerticalValue(selectedRow); - centerWindow = false; - } - } - updatePositions(); - - // Rescale the scroll bars - setBottomValue(getChildren().size() - getHeight() + 1); - if (getBottomValue() < 0) { - setBottomValue(0); - } - if (getVerticalValue() > getBottomValue()) { - setVerticalValue(getBottomValue()); - } - setRightValue(maxLineWidth - getWidth() + 3); - if (getRightValue() < 0) { - setRightValue(0); - } - if (getHorizontalValue() > getRightValue()) { - setHorizontalValue(getRightValue()); - } - getChildren().add(hScroller); - getChildren().add(vScroller); } - /** - * Update the Y positions of all the children items. - */ - private void updatePositions() { - if (treeRoot == null) { - return; - } - - int begin = getVerticalValue(); - int topY = 0; - - // 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++) { - if (!(getChildren().get(i) instanceof TTreeItem)) { - // Skip the scrollbars - continue; - } - TTreeItem item = (TTreeItem) getChildren().get(i); - - if (p != null) { - item.keyboardPrevious = p; - p.keyboardNext = item; - } - p = item; - - if (i < begin) { - // Render invisible - item.setEnabled(false); - item.setInvisible(true); - continue; - } - - if (topY >= getHeight() - 1) { - // Render invisible - item.setEnabled(false); - item.setInvisible(true); - continue; - } - - item.setY(topY); - item.setEnabled(true); - item.setInvisible(false); - item.setWidth(getWidth() - 1); - topY++; - } - - } + // ------------------------------------------------------------------------ + // Event handlers --------------------------------------------------------- + // ------------------------------------------------------------------------ /** * Handle mouse press events. @@ -300,11 +121,13 @@ public class TTreeView extends TScrollableWidget { } else if (mouse.isMouseWheelDown()) { verticalIncrement(); } else { - // Pass to children + // Pass to the TreeView or scrollbars super.onMouseDown(mouse); } - // Update the screen after the scrollbars have moved + // Update the view to reflect the new scrollbar positions + treeView.setTopLine(getVerticalValue()); + treeView.setLeftColumn(getHorizontalValue()); reflowData(); } @@ -315,10 +138,28 @@ public class TTreeView extends TScrollableWidget { */ @Override public void onMouseUp(final TMouseEvent mouse) { - // Pass to children - super.onMouseDown(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 screen after any thing has expanded/contracted + // Update the view to reflect the new scrollbar positions + treeView.setTopLine(getVerticalValue()); + treeView.setLeftColumn(getHorizontalValue()); reflowData(); } @@ -359,36 +200,29 @@ public class TTreeView extends TScrollableWidget { || keypress.equals(kbAltPgDn) ) { bigVerticalIncrement(); - } else if (keypress.equals(kbHome)) { - toTop(); - } else if (keypress.equals(kbEnd)) { - toBottom(); - } else if (keypress.equals(kbEnter)) { - if (selectedItem != null) { - dispatch(); + } else if (keypress.equals(kbPgDn)) { + for (int i = 0; i < getHeight() - 2; i++) { + treeView.onKeypress(new TKeypressEvent(TKeypress.kbDown)); } - } else if (keypress.equals(kbUp)) { - // Select the previous item - if (selectedItem != null) { - TTreeItem oldItem = selectedItem; - if (selectedItem.keyboardPrevious != null) { - setSelected(selectedItem.keyboardPrevious); - if (oldItem.getY() == 0) { - verticalDecrement(); - } - } - } - } else if (keypress.equals(kbDown)) { - // Select the next item - if (selectedItem != null) { - TTreeItem oldItem = selectedItem; - if (selectedItem.keyboardNext != null) { - setSelected(selectedItem.keyboardNext); - if (oldItem.getY() == getHeight() - 2) { - verticalIncrement(); - } - } + 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; @@ -396,17 +230,157 @@ public class TTreeView extends TScrollableWidget { || keypress.equals(kbBackTab)) { getParent().switchWidget(false); return; - } else if (selectedItem != null) { - // Give the TTreeItem a chance to handle arrow keys - selectedItem.onKeypress(keypress); } else { - // Pass other keys (tab etc.) on to TWidget's handler. - super.onKeypress(keypress); + treeView.onKeypress(keypress); + + // Update the scrollbars to reflect the new data position + reflowData(); return; } - // Update the screen after any thing has expanded/contracted + // 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 = item.getText().length() + + 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..2adf616 --- /dev/null +++ b/src/jexer/ttree/TTreeViewWindow.java @@ -0,0 +1,400 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2017 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN 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.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; + } + + 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 = item.getText().length() + + 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..72dc8da --- /dev/null +++ b/src/jexer/ttree/package-info.java @@ -0,0 +1,33 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2017 Kevin Lamonte + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF 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;