Prep for 2019 release
authorKevin Lamonte <kevin.lamonte@gmail.com>
Mon, 22 Oct 2018 02:18:05 +0000 (21:18 -0500)
committerKevin Lamonte <kevin.lamonte@gmail.com>
Mon, 22 Oct 2018 02:18:05 +0000 (21:18 -0500)
135 files changed:
.gitignore
README.md
docs/TODO.md
docs/worklog.md
pom.xml
src/jexer/Scrollable.java
src/jexer/TAction.java
src/jexer/TApplication.java
src/jexer/TApplication.properties
src/jexer/TButton.java
src/jexer/TCalendar.java
src/jexer/TCheckBox.java
src/jexer/TComboBox.java
src/jexer/TCommand.java
src/jexer/TDesktop.java
src/jexer/TDirectoryList.java
src/jexer/TEditColorThemeWindow.java
src/jexer/TEditorWidget.java
src/jexer/TEditorWindow.java
src/jexer/TField.java
src/jexer/TFileOpenBox.java
src/jexer/TFileOpenBox.properties
src/jexer/THScroller.java
src/jexer/TImage.java [new file with mode: 0644]
src/jexer/TImageWindow.java [new file with mode: 0644]
src/jexer/TInputBox.java
src/jexer/TKeypress.java
src/jexer/TLabel.java
src/jexer/TList.java
src/jexer/TMessageBox.java
src/jexer/TPasswordField.java
src/jexer/TProgressBar.java
src/jexer/TRadioButton.java
src/jexer/TRadioGroup.java
src/jexer/TScrollableWidget.java
src/jexer/TScrollableWindow.java
src/jexer/TSpinner.java
src/jexer/TStatusBar.java
src/jexer/TTerminalWindow.java
src/jexer/TText.java
src/jexer/TTimer.java
src/jexer/TVScroller.java
src/jexer/TWidget.java
src/jexer/TWindow.java
src/jexer/backend/Backend.java
src/jexer/backend/ECMA48Backend.java
src/jexer/backend/ECMA48Terminal.java
src/jexer/backend/GenericBackend.java
src/jexer/backend/LogicalScreen.java
src/jexer/backend/MultiBackend.java
src/jexer/backend/MultiScreen.java
src/jexer/backend/Screen.java
src/jexer/backend/SessionInfo.java
src/jexer/backend/SwingBackend.java
src/jexer/backend/SwingComponent.java
src/jexer/backend/SwingSessionInfo.java
src/jexer/backend/SwingTerminal.java
src/jexer/backend/TSessionInfo.java
src/jexer/backend/TTYSessionInfo.java
src/jexer/backend/TWindowBackend.java
src/jexer/backend/TerminalReader.java
src/jexer/backend/package-info.java
src/jexer/bits/Cell.java
src/jexer/bits/CellAttributes.java
src/jexer/bits/Color.java
src/jexer/bits/ColorTheme.java
src/jexer/bits/GraphicsChars.java
src/jexer/bits/MnemonicString.java
src/jexer/bits/StringUtils.java
src/jexer/bits/package-info.java
src/jexer/demos/Demo1.java
src/jexer/demos/Demo2.java
src/jexer/demos/Demo2.properties [new file with mode: 0644]
src/jexer/demos/Demo3.java
src/jexer/demos/Demo4.java
src/jexer/demos/Demo5.java
src/jexer/demos/Demo5.properties [new file with mode: 0644]
src/jexer/demos/Demo6.java
src/jexer/demos/DemoApplication.java
src/jexer/demos/DemoApplication.properties [new file with mode: 0644]
src/jexer/demos/DemoCheckBoxWindow.java
src/jexer/demos/DemoCheckBoxWindow.properties [new file with mode: 0644]
src/jexer/demos/DemoEditorWindow.java
src/jexer/demos/DemoEditorWindow.properties [new file with mode: 0644]
src/jexer/demos/DemoMainWindow.java
src/jexer/demos/DemoMainWindow.properties [new file with mode: 0644]
src/jexer/demos/DemoMsgBoxWindow.java
src/jexer/demos/DemoMsgBoxWindow.properties [new file with mode: 0644]
src/jexer/demos/DemoTextFieldWindow.java
src/jexer/demos/DemoTextFieldWindow.properties [new file with mode: 0644]
src/jexer/demos/DemoTextWindow.java
src/jexer/demos/DemoTextWindow.properties [new file with mode: 0644]
src/jexer/demos/DemoTreeViewWindow.java
src/jexer/demos/DemoTreeViewWindow.properties [new file with mode: 0644]
src/jexer/demos/DesktopDemo.java
src/jexer/demos/DesktopDemoApplication.java
src/jexer/demos/DesktopDemoApplication.properties [new file with mode: 0644]
src/jexer/demos/package-info.java
src/jexer/event/TCommandEvent.java
src/jexer/event/TInputEvent.java
src/jexer/event/TKeypressEvent.java
src/jexer/event/TMenuEvent.java
src/jexer/event/TMouseEvent.java
src/jexer/event/TResizeEvent.java
src/jexer/event/package-info.java
src/jexer/io/ReadTimeoutException.java
src/jexer/io/TimeoutInputStream.java
src/jexer/io/package-info.java
src/jexer/menu/TMenu.java
src/jexer/menu/TMenuItem.java
src/jexer/menu/TMenuSeparator.java
src/jexer/menu/TSubMenu.java
src/jexer/menu/package-info.java
src/jexer/net/TelnetInputStream.java
src/jexer/net/TelnetOutputStream.java
src/jexer/net/TelnetServerSocket.java
src/jexer/net/TelnetSocket.java
src/jexer/net/package-info.java
src/jexer/package-info.java
src/jexer/teditor/Document.java
src/jexer/teditor/Highlighter.java
src/jexer/teditor/Line.java
src/jexer/teditor/Word.java
src/jexer/teditor/package-info.java
src/jexer/tterminal/DECCharacterSets.java
src/jexer/tterminal/DisplayLine.java
src/jexer/tterminal/DisplayListener.java
src/jexer/tterminal/ECMA48.java
src/jexer/tterminal/package-info.java
src/jexer/ttree/TDirectoryTreeItem.java
src/jexer/ttree/TTreeItem.java
src/jexer/ttree/TTreeView.java
src/jexer/ttree/TTreeViewWidget.java
src/jexer/ttree/TTreeViewWindow.java
src/jexer/ttree/package-info.java

index 704dab2a094b5c47aa778c64d2f47ab3eb982aa8..733399a10f68ceee79562c7ed7f8903fecad83f2 100644 (file)
@@ -23,3 +23,6 @@ hs_err_pid*
 # Scratch space
 misc/**
 /.project~
+
+pmd.bash
+pmd-results.html
index dc976599bf6cffeb4d37a2d48a3d0bc0932d5d77..009ec6f99a5dfa9b23b530df024ec592e8e1da6a 100644 (file)
--- a/README.md
+++ b/README.md
@@ -14,8 +14,8 @@ Jexer currently supports three backends:
 * System.in/out to a command-line ECMA-48 / ANSI X3.64 type terminal
   (tested on Linux + xterm).  I/O is handled through terminal escape
   sequences generated by the library itself: ncurses is not required
-  or linked to.  xterm mouse tracking using UTF8 and SGR coordinates
-  are supported.  For the demo application, this is the default
+  or linked to.  xterm mouse tracking is supported using both UTF8 and
+  SGR coordinates.  For the demo application, this is the default
   backend on non-Windows/non-Mac platforms.
 
 * The same command-line ECMA-48 / ANSI X3.64 type terminal as above,
@@ -36,7 +36,7 @@ constructor.  See Demo5 and Demo6 for examples of other backends.
 
 The Jexer homepage, which includes additional information and binary
 release downloads, is at: https://jexer.sourceforge.io .  The Jexer
-source code is hosted at: https://github.com/klamonte/jexer .
+source code is hosted at: https://gitlab.com/klamonte/jexer .
 
 
 
@@ -203,7 +203,14 @@ The following properties control features of Jexer:
   Used by jexer.TTerminalWindow.  If true, spawn shell using the
   'ptypipe' utility rather than 'script'.  This permits terminals to
   resize with the window.  ptypipe is a separate C language utility,
-  available at https://github.com/klamonte/ptypipe.  Default: false.
+  available at https://gitlab.com/klamonte/ptypipe.  Default: false.
+
+  jexer.ECMA48.rgbColor
+  ---------------------
+
+  Used by jexer.backend.ECMA48Terminal.  If true, emit T.416-style RGB
+  colors for normal system colors.  This is expensive in bandwidth,
+  and potentially terrible looking for non-xterms.  Default: false.
 
 
 
@@ -234,7 +241,7 @@ ambiguous.  This section describes such issues.
   - TTerminalWindow can only notify the child process of changes in
     window size if using the 'ptypipe' utility, due to Java's lack of
     support for forkpty() and similar.  ptypipe is available at
-    https://github.com/klamonte/ptypipe.
+    https://gitlab.com/klamonte/ptypipe.
 
   - Java's InputStreamReader as used by the ECMA48 backend requires a
     valid UTF-8 stream.  The default X10 encoding for mouse
index 55b62386c52b6c6dfa5bdc12149cda1782a91962..ab4e7c8ccb61df71c2e42685fffd3f961469cc65 100644 (file)
@@ -106,7 +106,7 @@ Update written by date to current year:
     All code headers
     VERSION
 
-Tag github
+Tag gitlab
 
 Upload to SF
 
index d544788b268af876b338a861a7e5c2ab884dbebe..5f7e192aa2a2458a7ecee4b8888915552489c8f0 100644 (file)
@@ -1,6 +1,18 @@
 Jexer Work Log
 ==============
 
+October 21, 2018
+
+All future work for this project has been moved to GitLab.  GitHub
+will remain up for a few months longer, until the next release comes
+out.
+
+September 18, 2018
+
+Collecting up a few bug fixes involving race conditions: message box
+(and input box), TimeoutInputStream, window close.  Will get these
+uploaded to GitHub in a little while.
+
 July 13, 2018
 
 This project isn't dead, I swear!
diff --git a/pom.xml b/pom.xml
index 2118e8973832109e8f25fa7da23df8bec64f02c8..524c4aa53a04eb8c89a38179133e5d8dac85a8eb 100644 (file)
--- a/pom.xml
+++ b/pom.xml
@@ -2,13 +2,13 @@
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
   <modelVersion>4.0.0</modelVersion>
-  <groupId>com.github.klamonte</groupId>
+  <groupId>com.gitlab.klamonte</groupId>
   <artifactId>jexer</artifactId>
   <packaging>jar</packaging>
   <name>Jexer</name>
   <description>Java Text User Interface library that resembles Turbo Vision</description>
   <version>0.0.6</version>
-  <url>https://github.com/klamonte/jexer</url>
+  <url>https://gitlab.com/klamonte/jexer</url>
 
   <licenses>
     <license>
   </properties>
 
   <scm>
-    <connection>scm:git:https://github.com/klamonte/jexer.git</connection>
-    <developerConnection>scm:git:https://github.com/klamonte/jexer.git</developerConnection>
-    <url>https://github.com/klamonte/jexer</url>
+    <connection>scm:git:https://gitlab.com/klamonte/jexer.git</connection>
+    <developerConnection>scm:git:https://gitlab.com/klamonte/jexer.git</developerConnection>
+    <url>https://gitlab.com/klamonte/jexer</url>
     <tag>HEAD</tag>
   </scm>
 
   <issueManagement>
-    <system>github</system>
-    <url>https://github.com/klamonte/jexer/issues</url>
+    <system>gitlab</system>
+    <url>https://gitlab.com/klamonte/jexer/issues</url>
   </issueManagement>
 
   <build>
index 00c9ebefc2b967b991418e6d63bf6dd8ac5f402d..b844ca6f86027d2551c9cdba0755e51af76971fc 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index ea01ffa91d7a90635987af5af2975a13da507740..cc93819d538f708267bf179e8c5327f537c23de0 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index 96dad6cc0b0cece1541d43dccab333b813f233d0..96f91f2943696d38a7f6e4d72b8fab2036da3482 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
  */
 package jexer;
 
-import java.io.File;
 import java.io.InputStream;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.io.PrintWriter;
 import java.io.Reader;
 import java.io.UnsupportedEncodingException;
-import java.text.MessageFormat;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
-import java.util.ArrayList;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.ResourceBundle;
 
+import jexer.bits.Cell;
 import jexer.bits.CellAttributes;
 import jexer.bits.ColorTheme;
 import jexer.event.TCommandEvent;
@@ -54,8 +53,8 @@ import jexer.event.TMenuEvent;
 import jexer.event.TMouseEvent;
 import jexer.event.TResizeEvent;
 import jexer.backend.Backend;
-import jexer.backend.Screen;
 import jexer.backend.MultiBackend;
+import jexer.backend.Screen;
 import jexer.backend.SwingBackend;
 import jexer.backend.ECMA48Backend;
 import jexer.backend.TWindowBackend;
@@ -160,6 +159,21 @@ public class TApplication implements Runnable {
      */
     private int oldMouseY;
 
+    /**
+     * Old drawn version of mouse coordinate X.
+     */
+    private int oldDrawnMouseX;
+
+    /**
+     * Old drawn version mouse coordinate Y.
+     */
+    private int oldDrawnMouseY;
+
+    /**
+     * Old drawn version mouse cell.
+     */
+    private Cell oldDrawnMouseCell = new Cell();
+
     /**
      * The last mouse up click time, used to determine if this is a mouse
      * double-click.
@@ -266,6 +280,16 @@ public class TApplication implements Runnable {
      */
     private boolean focusFollowsMouse = false;
 
+    /**
+     * The images that might be displayed.  Note package private access.
+     */
+    private List<TImage> images;
+
+    /**
+     * The list of commands to run before the next I/O check.
+     */
+    private List<Runnable> invokeLaters = new LinkedList<Runnable>();
+
     /**
      * WidgetEventHandler is the main event consumer loop.  There are at most
      * two such threads in existence: the primary for normal case and a
@@ -300,6 +324,21 @@ public class TApplication implements Runnable {
          * The consumer loop.
          */
         public void run() {
+            // Wrap everything in a try, so that if we go belly up we can let
+            // the user have their terminal back.
+            try {
+                runImpl();
+            } catch (Throwable t) {
+                this.application.restoreConsole();
+                t.printStackTrace();
+                this.application.exit();
+            }
+        }
+
+        /**
+         * The consumer loop.
+         */
+        private void runImpl() {
             boolean first = true;
 
             // Loop forever
@@ -327,9 +366,10 @@ public class TApplication implements Runnable {
                         }
 
                         if (debugThreads) {
-                            System.err.printf("%d %s %s sleep %d millis\n",
+                            System.err.printf("%d %s %s %s sleep %d millis\n",
                                 System.currentTimeMillis(), this,
-                                primary ? "primary" : "secondary", timeout);
+                                primary ? "primary" : "secondary",
+                                Thread.currentThread(), timeout);
                         }
 
                         synchronized (this) {
@@ -337,9 +377,10 @@ public class TApplication implements Runnable {
                         }
 
                         if (debugThreads) {
-                            System.err.printf("%d %s %s AWAKE\n",
+                            System.err.printf("%d %s %s %s AWAKE\n",
                                 System.currentTimeMillis(), this,
-                                primary ? "primary" : "secondary");
+                                primary ? "primary" : "secondary",
+                                Thread.currentThread());
                         }
 
                         if ((!primary)
@@ -385,15 +426,16 @@ public class TApplication implements Runnable {
                     ) {
                         // Secondary thread, time to exit.
 
+                        // Eliminate my reference so that wakeEventHandler()
+                        // resumes working on the primary.
+                        application.secondaryEventHandler = null;
+
                         // DO NOT UNLOCK.  Primary thread just came back from
                         // primaryHandleEvent() and will unlock in the else
                         // block below.  Just wake it up.
                         synchronized (application.primaryEventHandler) {
                             application.primaryEventHandler.notify();
                         }
-                        // Now eliminate my reference so that
-                        // wakeEventHandler() resumes working on the primary.
-                        application.secondaryEventHandler = null;
 
                         // All done!
                         return;
@@ -552,15 +594,16 @@ public class TApplication implements Runnable {
     private void TApplicationImpl() {
         theme           = new ColorTheme();
         desktopBottom   = getScreen().getHeight() - 1;
-        fillEventQueue  = new ArrayList<TInputEvent>();
-        drainEventQueue = new ArrayList<TInputEvent>();
+        fillEventQueue  = new LinkedList<TInputEvent>();
+        drainEventQueue = new LinkedList<TInputEvent>();
         windows         = new LinkedList<TWindow>();
-        menus           = new LinkedList<TMenu>();
-        subMenus        = new LinkedList<TMenu>();
+        menus           = new ArrayList<TMenu>();
+        subMenus        = new ArrayList<TMenu>();
         timers          = new LinkedList<TTimer>();
         accelerators    = new HashMap<TKeypress, TMenuItem>();
-        menuItems       = new ArrayList<TMenuItem>();
+        menuItems       = new LinkedList<TMenuItem>();
         desktop         = new TDesktop(this);
+        images          = new LinkedList<TImage>();
 
         // Special case: the Swing backend needs to have a timer to drive its
         // blink state.
@@ -617,14 +660,14 @@ public class TApplication implements Runnable {
                     try {
                         if (debugThreads) {
                             System.err.println(System.currentTimeMillis() +
-                                " MAIN sleep");
+                                " " + Thread.currentThread() + " MAIN sleep");
                         }
 
                         this.wait();
 
                         if (debugThreads) {
                             System.err.println(System.currentTimeMillis() +
-                                " MAIN AWAKE");
+                                " " + Thread.currentThread() + " MAIN AWAKE");
                         }
                     } catch (InterruptedException e) {
                         // I'm awake and don't care why, let's see what's
@@ -694,7 +737,8 @@ public class TApplication implements Runnable {
         if (command.equals(cmExit)) {
             if (messageBox(i18n.getString("exitDialogTitle"),
                     i18n.getString("exitDialogText"),
-                    TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) {
+                    TMessageBox.Type.YESNO).isYes()) {
+
                 exit();
             }
             return true;
@@ -744,7 +788,8 @@ public class TApplication implements Runnable {
         if (menu.getId() == TMenu.MID_EXIT) {
             if (messageBox(i18n.getString("exitDialogTitle"),
                     i18n.getString("exitDialogText"),
-                    TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) {
+                    TMessageBox.Type.YESNO).isYes()) {
+
                 exit();
             }
             return true;
@@ -767,11 +812,8 @@ public class TApplication implements Runnable {
             closeAllWindows();
             return true;
         }
-        if (menu.getId() == TMenu.MID_ABOUT) {
-            showAboutDialog();
-            return true;
-        }
         if (menu.getId() == TMenu.MID_REPAINT) {
+            getScreen().clearPhysical();
             doRepaint();
             return true;
         }
@@ -911,7 +953,8 @@ public class TApplication implements Runnable {
     private void primaryHandleEvent(final TInputEvent event) {
 
         if (debugEvents) {
-            System.err.printf("Handle event: %s\n", event);
+            System.err.printf("%s primaryHandleEvent: %s\n",
+                Thread.currentThread(), event);
         }
         TMouseEvent doubleClick = null;
 
@@ -1094,6 +1137,11 @@ public class TApplication implements Runnable {
     private void secondaryHandleEvent(final TInputEvent event) {
         TMouseEvent doubleClick = null;
 
+        if (debugEvents) {
+            System.err.printf("%s secondaryHandleEvent: %s\n",
+                Thread.currentThread(), event);
+        }
+
         // Peek at the mouse position
         if (event instanceof TMouseEvent) {
             TMouseEvent mouse = (TMouseEvent) event;
@@ -1161,6 +1209,11 @@ public class TApplication implements Runnable {
      * Yield to the secondary thread.
      */
     public final void yield() {
+        if (debugThreads) {
+            System.err.printf(System.currentTimeMillis() + " " +
+                Thread.currentThread() + " yield()\n");
+        }
+
         assert (secondaryEventReceiver != null);
 
         while (secondaryEventReceiver != null) {
@@ -1216,6 +1269,15 @@ public class TApplication implements Runnable {
         if (desktop != null) {
             desktop.onIdle();
         }
+
+        // Run any invokeLaters
+        synchronized (invokeLaters) {
+            for (Runnable invoke: invokeLaters) {
+                invoke.run();
+            }
+            invokeLaters.clear();
+        }
+
     }
 
     /**
@@ -1242,6 +1304,31 @@ public class TApplication implements Runnable {
     // TApplication -----------------------------------------------------------
     // ------------------------------------------------------------------------
 
+    /**
+     * Place a command on the run queue, and run it before the next round of
+     * checking I/O.
+     *
+     * @param command the command to run later
+     */
+    public void invokeLater(final Runnable command) {
+        synchronized (invokeLaters) {
+            invokeLaters.add(command);
+        }
+    }
+
+    /**
+     * Restore the console to sane defaults.  This is meant to be used for
+     * improper exits (e.g. a caught exception in main()), and should not be
+     * necessary for normal program termination.
+     */
+    public void restoreConsole() {
+        if (backend != null) {
+            if (backend instanceof ECMA48Backend) {
+                backend.shutdown();
+            }
+        }
+    }
+
     /**
      * Get the Backend.
      *
@@ -1340,7 +1427,7 @@ public class TApplication implements Runnable {
      * @return a copy of the list of windows for this application
      */
     public final List<TWindow> getAllWindows() {
-        List<TWindow> result = new LinkedList<TWindow>();
+        List<TWindow> result = new ArrayList<TWindow>();
         result.addAll(windows);
         return result;
     }
@@ -1365,16 +1452,6 @@ public class TApplication implements Runnable {
         this.focusFollowsMouse = focusFollowsMouse;
     }
 
-    /**
-     * Display the about dialog.
-     */
-    protected void showAboutDialog() {
-        messageBox(i18n.getString("aboutDialogTitle"),
-            MessageFormat.format(i18n.getString("aboutDialogText"),
-                this.getClass().getPackage().getImplementationVersion()),
-            TMessageBox.Type.OK);
-    }
-
     // ------------------------------------------------------------------------
     // Screen refresh loop ----------------------------------------------------
     // ------------------------------------------------------------------------
@@ -1390,18 +1467,22 @@ public class TApplication implements Runnable {
             System.err.printf("%d %s invertCell() %d %d\n",
                 System.currentTimeMillis(), Thread.currentThread(), x, y);
         }
-        CellAttributes attr = getScreen().getAttrXY(x, y);
-        if (attr.getForeColorRGB() < 0) {
-            attr.setForeColor(attr.getForeColor().invert());
-        } else {
-            attr.setForeColorRGB(attr.getForeColorRGB() ^ 0x00ffffff);
-        }
-        if (attr.getBackColorRGB() < 0) {
-            attr.setBackColor(attr.getBackColor().invert());
+        Cell cell = getScreen().getCharXY(x, y);
+        if (cell.isImage()) {
+            cell.invertImage();
         } else {
-            attr.setBackColorRGB(attr.getBackColorRGB() ^ 0x00ffffff);
+            if (cell.getForeColorRGB() < 0) {
+                cell.setForeColor(cell.getForeColor().invert());
+            } else {
+                cell.setForeColorRGB(cell.getForeColorRGB() ^ 0x00ffffff);
+            }
+            if (cell.getBackColorRGB() < 0) {
+                cell.setBackColor(cell.getBackColor().invert());
+            } else {
+                cell.setBackColorRGB(cell.getBackColorRGB() ^ 0x00ffffff);
+            }
         }
-        getScreen().putAttrXY(x, y, attr, false);
+        getScreen().putCharXY(x, y, cell);
     }
 
     /**
@@ -1415,25 +1496,62 @@ public class TApplication implements Runnable {
                 System.currentTimeMillis(), Thread.currentThread());
         }
 
+        // I don't think this does anything useful anymore...
         if (!repaint) {
             if (debugThreads) {
                 System.err.printf("%d %s drawAll() !repaint\n",
                     System.currentTimeMillis(), Thread.currentThread());
             }
-            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 ((oldDrawnMouseX != mouseX) || (oldDrawnMouseY != mouseY)) {
+                if (debugThreads) {
+                    System.err.printf("%d %s drawAll() !repaint MOUSE\n",
+                        System.currentTimeMillis(), Thread.currentThread());
                 }
-                if (getScreen().isDirty()) {
-                    backend.flushScreen();
+
+                // The only thing that has happened is the mouse moved.
+
+                // Redraw the old cell at that position, and save the cell at
+                // the new mouse position.
+                if (debugThreads) {
+                    System.err.printf("%d %s restoreImage() %d %d\n",
+                        System.currentTimeMillis(), Thread.currentThread(),
+                        oldDrawnMouseX, oldDrawnMouseY);
                 }
-                return;
+                oldDrawnMouseCell.restoreImage();
+                getScreen().putCharXY(oldDrawnMouseX, oldDrawnMouseY,
+                    oldDrawnMouseCell);
+                oldDrawnMouseCell = getScreen().getCharXY(mouseX, mouseY);
+                if ((images.size() > 0) && (backend instanceof ECMA48Backend)) {
+                    // Special case: the entire row containing the mouse has
+                    // to be re-drawn if it has any image data, AND any rows
+                    // in between.
+                    if (oldDrawnMouseY != mouseY) {
+                        for (int i = oldDrawnMouseY; ;) {
+                            getScreen().unsetImageRow(i);
+                            if (i == mouseY) {
+                                break;
+                            }
+                            if (oldDrawnMouseY < mouseY) {
+                                i++;
+                            } else {
+                                i--;
+                            }
+                        }
+                    } else {
+                        getScreen().unsetImageRow(mouseY);
+                    }
+                }
+
+                // Draw mouse at the new position.
+                invertCell(mouseX, mouseY);
+
+                oldDrawnMouseX = mouseX;
+                oldDrawnMouseY = mouseY;
+            }
+            if ((images.size() > 0) || getScreen().isDirty()) {
+                backend.flushScreen();
             }
+            return;
         }
 
         if (debugThreads) {
@@ -1453,7 +1571,7 @@ public class TApplication implements Runnable {
         }
 
         // Draw each window in reverse Z order
-        List<TWindow> sorted = new LinkedList<TWindow>(windows);
+        List<TWindow> sorted = new ArrayList<TWindow>(windows);
         Collections.sort(sorted);
         TWindow topLevel = null;
         if (sorted.size() > 0) {
@@ -1494,7 +1612,7 @@ public class TApplication implements Runnable {
                 0, menu.getMnemonic().getShortcut(), menuMnemonicColor);
 
             if (menu.isActive()) {
-                menu.drawChildren();
+                ((TWindow) menu).drawChildren();
                 // Reset the screen clipping so we can draw the next title.
                 getScreen().resetClipping();
             }
@@ -1504,8 +1622,9 @@ public class TApplication implements Runnable {
         for (TMenu menu: subMenus) {
             // Reset the screen clipping so we can draw the next sub-menu.
             getScreen().resetClipping();
-            menu.drawChildren();
+            ((TWindow) menu).drawChildren();
         }
+        getScreen().resetClipping();
 
         // Draw the status bar of the top-level window
         TStatusBar statusBar = null;
@@ -1525,9 +1644,34 @@ public class TApplication implements Runnable {
         }
 
         // Draw the mouse pointer
+        if (debugThreads) {
+            System.err.printf("%d %s restoreImage() %d %d\n",
+                System.currentTimeMillis(), Thread.currentThread(),
+                oldDrawnMouseX, oldDrawnMouseY);
+        }
+        oldDrawnMouseCell = getScreen().getCharXY(mouseX, mouseY);
+        if ((images.size() > 0) && (backend instanceof ECMA48Backend)) {
+            // Special case: the entire row containing the mouse has to be
+            // re-drawn if it has any image data, AND any rows in between.
+            if (oldDrawnMouseY != mouseY) {
+                for (int i = oldDrawnMouseY; ;) {
+                    getScreen().unsetImageRow(i);
+                    if (i == mouseY) {
+                        break;
+                    }
+                    if (oldDrawnMouseY < mouseY) {
+                        i++;
+                    } else {
+                        i--;
+                    }
+                }
+            } else {
+                getScreen().unsetImageRow(mouseY);
+            }
+        }
         invertCell(mouseX, mouseY);
-        oldMouseX = mouseX;
-        oldMouseY = mouseY;
+        oldDrawnMouseX = mouseX;
+        oldDrawnMouseY = mouseY;
 
         // Place the cursor if it is visible
         if (!menuIsActive) {
@@ -1543,9 +1687,8 @@ public class TApplication implements Runnable {
                             activeWidget.getCursorAbsoluteY());
                         cursor = true;
                     } else {
-                        getScreen().putCursor(false,
-                            activeWidget.getCursorAbsoluteX(),
-                            activeWidget.getCursorAbsoluteY());
+                        // Turn off the cursor.  Also place it at 0,0.
+                        getScreen().putCursor(false, 0, 0);
                         cursor = false;
                     }
                 }
@@ -1558,7 +1701,7 @@ public class TApplication implements Runnable {
         }
 
         // Flush the screen contents
-        if (getScreen().isDirty()) {
+        if ((images.size() > 0) || getScreen().isDirty()) {
             backend.flushScreen();
         }
 
@@ -1693,7 +1836,16 @@ public class TApplication implements Runnable {
             assert (activeWindow.getZ() == 0);
 
             activeWindow.setActive(false);
-            activeWindow.setZ(window.getZ());
+
+            // Increment every window Z that is on top of window
+            for (TWindow w: windows) {
+                if (w == window) {
+                    continue;
+                }
+                if (w.getZ() < window.getZ()) {
+                    w.setZ(w.getZ() + 1);
+                }
+            }
 
             // Unset activeWindow now before unfocus, so that a window
             // lifecycle change inside onUnfocus() doesn't call
@@ -1792,6 +1944,10 @@ public class TApplication implements Runnable {
             return;
         }
 
+        // Let window know that it is about to be closed, while it is still
+        // visible on screen.
+        window.onPreClose();
+
         synchronized (windows) {
             // Whatever window might be moving/dragging, stop it now.
             for (TWindow w: windows) {
@@ -2051,7 +2207,7 @@ public class TApplication implements Runnable {
             int newHeight1 = ((getScreen().getHeight() - 1) / b);
             int newHeight2 = ((getScreen().getHeight() - 1) / (b + c));
 
-            List<TWindow> sorted = new LinkedList<TWindow>(windows);
+            List<TWindow> sorted = new ArrayList<TWindow>(windows);
             Collections.sort(sorted);
             Collections.reverse(sorted);
             for (int i = 0; i < sorted.size(); i++) {
@@ -2096,7 +2252,7 @@ public class TApplication implements Runnable {
             }
             int x = 0;
             int y = 1;
-            List<TWindow> sorted = new LinkedList<TWindow>(windows);
+            List<TWindow> sorted = new ArrayList<TWindow>(windows);
             Collections.sort(sorted);
             Collections.reverse(sorted);
             for (TWindow window: sorted) {
@@ -2245,6 +2401,46 @@ public class TApplication implements Runnable {
         window.setY(windowY);
     }
 
+    // ------------------------------------------------------------------------
+    // TImage management ------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Add an image to the list.  Note package private access.
+     *
+     * @param image the image to add
+     * @throws IllegalArgumentException if the image is already used in
+     * another TApplication
+     */
+    final void addImage(final TImage image) {
+        if ((image.getApplication() != null)
+            && (image.getApplication() != this)
+        ) {
+            throw new IllegalArgumentException("Image " + image +
+                " is already " + "part of application " +
+                image.getApplication());
+        }
+        images.add(image);
+    }
+
+    /**
+     * Remove an image from the list.  Note package private access.
+     *
+     * @param image the image to remove
+     * @throws IllegalArgumentException if the image is already used in
+     * another TApplication
+     */
+    final void removeImage(final TImage image) {
+        if ((image.getApplication() != null)
+            && (image.getApplication() != this)
+        ) {
+            throw new IllegalArgumentException("Image " + image +
+                " is already " + "part of application " +
+                image.getApplication());
+        }
+        images.remove(image);
+    }
+
     // ------------------------------------------------------------------------
     // TMenu management -------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -2259,7 +2455,7 @@ public class TApplication implements Runnable {
      */
     private boolean mouseOnMenu(final TMouseEvent mouse) {
         assert (activeMenu != null);
-        List<TMenu> menus = new LinkedList<TMenu>(subMenus);
+        List<TMenu> menus = new ArrayList<TMenu>(subMenus);
         Collections.reverse(menus);
         for (TMenu menu: menus) {
             if (menu.mouseWouldHit(mouse)) {
@@ -2389,9 +2585,11 @@ public class TApplication implements Runnable {
                         assert (windows.get(0).isActive());
                         assert (windows.get(0) == activeWindow);
                         assert (!window.isActive());
-                        activeWindow.onUnfocus();
-                        activeWindow.setActive(false);
-                        activeWindow.setZ(window.getZ());
+                        if (activeWindow != null) {
+                            activeWindow.onUnfocus();
+                            activeWindow.setActive(false);
+                            activeWindow.setZ(window.getZ());
+                        }
                         activeWindow = window;
                         window.setZ(0);
                         window.setActive(true);
@@ -2430,7 +2628,7 @@ public class TApplication implements Runnable {
      * @return a copy of the menu list
      */
     public final List<TMenu> getAllMenus() {
-        return new LinkedList<TMenu>(menus);
+        return new ArrayList<TMenu>(menus);
     }
 
     /**
@@ -2501,10 +2699,14 @@ public class TApplication implements Runnable {
                 if (forward) {
                     if (i < menus.size() - 1) {
                         i++;
+                    } else {
+                        i = 0;
                     }
                 } else {
                     if (i > 0) {
                         i--;
+                    } else {
+                        i = menus.size() - 1;
                     }
                 }
                 activeMenu.setActive(false);
@@ -2557,6 +2759,7 @@ public class TApplication implements Runnable {
         for (TMenuItem item: menuItems) {
             if ((item.getId() >= lower) && (item.getId() <= upper)) {
                 item.setEnabled(false);
+                item.getParent().activate(0);
             }
         }
     }
@@ -2570,6 +2773,7 @@ public class TApplication implements Runnable {
         for (TMenuItem item: menuItems) {
             if (item.getId() == id) {
                 item.setEnabled(true);
+                item.getParent().activate(0);
             }
         }
     }
@@ -2585,6 +2789,7 @@ public class TApplication implements Runnable {
         for (TMenuItem item: menuItems) {
             if ((item.getId() >= lower) && (item.getId() <= upper)) {
                 item.setEnabled(true);
+                item.getParent().activate(0);
             }
         }
     }
@@ -2897,6 +3102,20 @@ public class TApplication implements Runnable {
         return openTerminal(x, y, TWindow.RESIZABLE);
     }
 
+    /**
+     * Convenience function to open a terminal window.
+     *
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param closeOnExit if true, close the window when the command exits
+     * @return the terminal new window
+     */
+    public final TTerminalWindow openTerminal(final int x, final int y,
+        final boolean closeOnExit) {
+
+        return openTerminal(x, y, TWindow.RESIZABLE, closeOnExit);
+    }
+
     /**
      * Convenience function to open a terminal window.
      *
@@ -2911,6 +3130,21 @@ public class TApplication implements Runnable {
         return new TTerminalWindow(this, x, y, flags);
     }
 
+    /**
+     * Convenience function to open a terminal window.
+     *
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param flags mask of CENTERED, MODAL, or RESIZABLE
+     * @param closeOnExit if true, close the window when the command exits
+     * @return the terminal new window
+     */
+    public final TTerminalWindow openTerminal(final int x, final int y,
+        final int flags, final boolean closeOnExit) {
+
+        return new TTerminalWindow(this, x, y, flags, closeOnExit);
+    }
+
     /**
      * Convenience function to open a terminal window and execute a custom
      * command line inside it.
@@ -2926,6 +3160,22 @@ public class TApplication implements Runnable {
         return openTerminal(x, y, TWindow.RESIZABLE, commandLine);
     }
 
+    /**
+     * Convenience function to open a terminal window and execute a custom
+     * command line inside it.
+     *
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param commandLine the command line to execute
+     * @param closeOnExit if true, close the window when the command exits
+     * @return the terminal new window
+     */
+    public final TTerminalWindow openTerminal(final int x, final int y,
+        final String commandLine, final boolean closeOnExit) {
+
+        return openTerminal(x, y, TWindow.RESIZABLE, commandLine, closeOnExit);
+    }
+
     /**
      * Convenience function to open a terminal window and execute a custom
      * command line inside it.
@@ -2942,6 +3192,23 @@ public class TApplication implements Runnable {
         return new TTerminalWindow(this, x, y, flags, command);
     }
 
+    /**
+     * Convenience function to open a terminal window and execute a custom
+     * command line inside it.
+     *
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param flags mask of CENTERED, MODAL, or RESIZABLE
+     * @param command the command line to execute
+     * @param closeOnExit if true, close the window when the command exits
+     * @return the terminal new window
+     */
+    public final TTerminalWindow openTerminal(final int x, final int y,
+        final int flags, final String [] command, final boolean closeOnExit) {
+
+        return new TTerminalWindow(this, x, y, flags, command, closeOnExit);
+    }
+
     /**
      * Convenience function to open a terminal window and execute a custom
      * command line inside it.
@@ -2958,6 +3225,24 @@ public class TApplication implements Runnable {
         return new TTerminalWindow(this, x, y, flags, commandLine.split("\\s"));
     }
 
+    /**
+     * Convenience function to open a terminal window and execute a custom
+     * command line inside it.
+     *
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param flags mask of CENTERED, MODAL, or RESIZABLE
+     * @param commandLine the command line to execute
+     * @param closeOnExit if true, close the window when the command exits
+     * @return the terminal new window
+     */
+    public final TTerminalWindow openTerminal(final int x, final int y,
+        final int flags, final String commandLine, final boolean closeOnExit) {
+
+        return new TTerminalWindow(this, x, y, flags, commandLine.split("\\s"),
+            closeOnExit);
+    }
+
     /**
      * Convenience function to spawn an file open box.
      *
@@ -2986,6 +3271,42 @@ public class TApplication implements Runnable {
         return box.getFilename();
     }
 
+    /**
+     * Convenience function to spawn a file open box.
+     *
+     * @param path path of selected file
+     * @param type one of the Type constants
+     * @param filter a string that files must match to be displayed
+     * @return the result of the new file open box
+     * @throws IOException of a java.io operation throws
+     */
+    public final String fileOpenBox(final String path,
+        final TFileOpenBox.Type type, final String filter) throws IOException {
+
+        ArrayList<String> filters = new ArrayList<String>();
+        filters.add(filter);
+
+        TFileOpenBox box = new TFileOpenBox(this, path, type, filters);
+        return box.getFilename();
+    }
+
+    /**
+     * Convenience function to spawn a file open box.
+     *
+     * @param path path of selected file
+     * @param type one of the Type constants
+     * @param filters a list of strings that files must match to be displayed
+     * @return the result of the new file open box
+     * @throws IOException of a java.io operation throws
+     */
+    public final String fileOpenBox(final String path,
+        final TFileOpenBox.Type type,
+        final List<String> filters) throws IOException {
+
+        TFileOpenBox box = new TFileOpenBox(this, path, type, filters);
+        return box.getFilename();
+    }
+
     /**
      * Convenience function to create a new window and make it active.
      * Window will be located at (0, 0).
@@ -3055,18 +3376,4 @@ public class TApplication implements Runnable {
         return window;
     }
 
-    /**
-     * Convenience function to open a file in an editor window and make it
-     * active.
-     *
-     * @param file the file to open
-     * @return the new editor window
-     * @throws IOException if a java.io operation throws
-     */
-    public final TEditorWindow addEditor(final File file) throws IOException {
-
-        TEditorWindow editor = new TEditorWindow(this, file);
-        return editor;
-    }
-
 }
index d43bc24a1a3dc05b503d70429488f630e9a7a465..bf9bcbea01c544e9475c536fa4263ad8566e5535 100644 (file)
@@ -10,6 +10,3 @@ helpMenuStatus=Access online help
 
 exitDialogTitle=Confirmation
 exitDialogText=Exit application?
-
-aboutDialogTitle=About
-aboutDialogText=Jexer Version {0}
index 8aa62705ea5b641dc5f76c003608dd7e221bdafd..29f37432826cf7fbc3556df35462117cc557ced8 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -226,25 +226,25 @@ public class TButton extends TWidget {
         }
 
         if (inButtonPress) {
-            getScreen().putCharXY(1, 0, ' ', buttonColor);
-            getScreen().putStringXY(2, 0, mnemonic.getRawLabel(), buttonColor);
-            getScreen().putCharXY(getWidth() - 1, 0, ' ', buttonColor);
+            putCharXY(1, 0, ' ', buttonColor);
+            putStringXY(2, 0, mnemonic.getRawLabel(), buttonColor);
+            putCharXY(getWidth() - 1, 0, ' ', buttonColor);
         } else {
-            getScreen().putCharXY(0, 0, ' ', buttonColor);
-            getScreen().putStringXY(1, 0, mnemonic.getRawLabel(), buttonColor);
-            getScreen().putCharXY(getWidth() - 2, 0, ' ', buttonColor);
+            putCharXY(0, 0, ' ', buttonColor);
+            putStringXY(1, 0, mnemonic.getRawLabel(), buttonColor);
+            putCharXY(getWidth() - 2, 0, ' ', buttonColor);
 
-            getScreen().putCharXY(getWidth() - 1, 0,
+            putCharXY(getWidth() - 1, 0,
                 GraphicsChars.CP437[0xDC], shadowColor);
-            getScreen().hLineXY(1, 1, getWidth() - 1,
+            hLineXY(1, 1, getWidth() - 1,
                 GraphicsChars.CP437[0xDF], shadowColor);
         }
         if (mnemonic.getShortcutIdx() >= 0) {
             if (inButtonPress) {
-                getScreen().putCharXY(2 + mnemonic.getShortcutIdx(), 0,
+                putCharXY(2 + mnemonic.getShortcutIdx(), 0,
                     mnemonic.getShortcut(), menuMnemonicColor);
             } else {
-                getScreen().putCharXY(1 + mnemonic.getShortcutIdx(), 0,
+                putCharXY(1 + mnemonic.getShortcutIdx(), 0,
                     mnemonic.getShortcut(), menuMnemonicColor);
             }
 
index c117fc1615b0ea9545c2f9103d86d3daf1720fe7..b5ecf4df4a3e4e381b7183a50ee0a444c03f6c99 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -232,27 +232,27 @@ public class TCalendar extends TWidget {
 
         // Fill in the interior background
         for (int i = 0; i < getHeight(); i++) {
-            getScreen().hLineXY(0, i, getWidth(), ' ', backgroundColor);
+            hLineXY(0, i, getWidth(), ' ', backgroundColor);
         }
 
         // Draw the title
         String title = String.format("%tB %tY", displayCalendar,
             displayCalendar);
         int titleLeft = (getWidth() - title.length() - 2) / 2;
-        getScreen().putCharXY(titleLeft, 0, ' ', titleColor);
-        getScreen().putStringXY(titleLeft + 1, 0, title, titleColor);
-        getScreen().putCharXY(titleLeft + title.length() + 1, 0, ' ',
+        putCharXY(titleLeft, 0, ' ', titleColor);
+        putStringXY(titleLeft + 1, 0, title, titleColor);
+        putCharXY(titleLeft + title.length() + 1, 0, ' ',
             titleColor);
 
         // Arrows
-        getScreen().putCharXY(1, 0, GraphicsChars.LEFTARROW, arrowColor);
-        getScreen().putCharXY(getWidth() - 2, 0, GraphicsChars.RIGHTARROW,
+        putCharXY(1, 0, GraphicsChars.LEFTARROW, arrowColor);
+        putCharXY(getWidth() - 2, 0, GraphicsChars.RIGHTARROW,
             arrowColor);
 
         /*
          * Now draw out the days.
          */
-        getScreen().putStringXY(0, 1, "  S   M   T   W   T   F   S ", dayColor);
+        putStringXY(0, 1, "  S   M   T   W   T   F   S ", dayColor);
         int lastDayNumber = displayCalendar.getActualMaximum(
                 Calendar.DAY_OF_MONTH);
         GregorianCalendar firstOfMonth = new GregorianCalendar();
@@ -274,10 +274,10 @@ public class TCalendar extends TWidget {
                 && (displayCalendar.get(Calendar.YEAR) == calendar.get(
                     Calendar.YEAR))
             ) {
-                getScreen().putStringXY(dayColumn, row,
+                putStringXY(dayColumn, row,
                     String.format(" %2d ", dayOfMonth), selectedDayColor);
             } else {
-                getScreen().putStringXY(dayColumn, row,
+                putStringXY(dayColumn, row,
                     String.format(" %2d ", dayOfMonth), dayColor);
             }
             dayColumn += 4;
index 188db7e622d51094aa728f1d716d950654036d35..84c2e41adf7a13d086e704b6490a13a0eccd5359 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -53,6 +53,11 @@ public class TCheckBox extends TWidget {
      */
     private String label;
 
+    /**
+     * If true, use the window's background color.
+     */
+    private boolean useWindowBackground = false;
+
     // ------------------------------------------------------------------------
     // Constructors -----------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -144,15 +149,19 @@ public class TCheckBox extends TWidget {
         } else {
             checkboxColor = getTheme().getColor("tcheckbox.inactive");
         }
+        if (useWindowBackground) {
+            CellAttributes background = getWindow().getBackground();
+            checkboxColor.setBackColor(background.getBackColor());
+        }
 
-        getScreen().putCharXY(0, 0, '[', checkboxColor);
+        putCharXY(0, 0, '[', checkboxColor);
         if (checked) {
-            getScreen().putCharXY(1, 0, GraphicsChars.CHECK, checkboxColor);
+            putCharXY(1, 0, GraphicsChars.CHECK, checkboxColor);
         } else {
-            getScreen().putCharXY(1, 0, ' ', checkboxColor);
+            putCharXY(1, 0, ' ', checkboxColor);
         }
-        getScreen().putCharXY(2, 0, ']', checkboxColor);
-        getScreen().putStringXY(4, 0, label, checkboxColor);
+        putCharXY(2, 0, ']', checkboxColor);
+        putStringXY(4, 0, label, checkboxColor);
     }
 
     // ------------------------------------------------------------------------
index 38224b89e8c3037ac94855324dff2dbc90b6ef0f..bb223c3a12aa4b6b3340bdaf544858a6025fde25 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -61,6 +61,11 @@ public class TComboBox extends TWidget {
      */
     private TAction updateAction = null;
 
+    /**
+     * If true, the field cannot be updated to a value not on the list.
+     */
+    private boolean limitToListValue = true;
+
     // ------------------------------------------------------------------------
     // Constructors -----------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -87,9 +92,11 @@ public class TComboBox extends TWidget {
         // Set parent and window
         super(parent, x, y, width, 1);
 
+        assert (values != null);
+
         this.updateAction = updateAction;
 
-        field = new TField(this, 0, 0, width - 1, false, "",
+        field = new TField(this, 0, 0, width - 3, false, "",
             updateAction, null);
         if (valuesIndex >= 0) {
             field.setText(values.get(valuesIndex));
@@ -102,18 +109,27 @@ public class TComboBox extends TWidget {
                     list.setEnabled(false);
                     list.setVisible(false);
                     TComboBox.this.setHeight(1);
-                    TComboBox.this.activate(field);
+                    if (TComboBox.this.limitToListValue == false) {
+                        TComboBox.this.activate(field);
+                    }
                     if (updateAction != null) {
                         updateAction.DO();
                     }
                 }
             }
         );
+        if (valuesIndex >= 0) {
+            list.setSelectedIndex(valuesIndex);
+        }
 
         list.setEnabled(false);
         list.setVisible(false);
         setHeight(1);
-        activate(field);
+        if (limitToListValue) {
+            field.setEnabled(false);
+        } else {
+            activate(field);
+        }
     }
 
     // ------------------------------------------------------------------------
@@ -128,7 +144,8 @@ public class TComboBox extends TWidget {
      */
     private boolean mouseOnArrow(final TMouseEvent mouse) {
         if ((mouse.getY() == 0)
-            && (mouse.getX() == getWidth() - 1)
+            && (mouse.getX() >= getWidth() - 3)
+            && (mouse.getX() <= getWidth() - 1)
         ) {
             return true;
         }
@@ -148,7 +165,9 @@ public class TComboBox extends TWidget {
                 list.setEnabled(false);
                 list.setVisible(false);
                 setHeight(1);
-                activate(field);
+                if (limitToListValue == false) {
+                    activate(field);
+                }
             } else {
                 list.setEnabled(true);
                 list.setVisible(true);
@@ -156,6 +175,9 @@ public class TComboBox extends TWidget {
                 activate(list);
             }
         }
+
+        // Pass to parent for the things we don't care about.
+        super.onMouseDown(mouse);
     }
 
     /**
@@ -165,6 +187,18 @@ public class TComboBox extends TWidget {
      */
     @Override
     public void onKeypress(final TKeypressEvent keypress) {
+        if (keypress.equals(kbEsc)) {
+            if (list.isActive()) {
+                list.setEnabled(false);
+                list.setVisible(false);
+                setHeight(1);
+                if (limitToListValue == false) {
+                    activate(field);
+                }
+                return;
+            }
+        }
+
         if (keypress.equals(kbAltDown)) {
             list.setEnabled(true);
             list.setVisible(true);
@@ -181,7 +215,9 @@ public class TComboBox extends TWidget {
                 list.setEnabled(false);
                 list.setVisible(false);
                 setHeight(1);
-                activate(field);
+                if (limitToListValue == false) {
+                    activate(field);
+                }
                 return;
             }
         }
@@ -201,13 +237,29 @@ public class TComboBox extends TWidget {
     public void draw() {
         CellAttributes comboBoxColor;
 
-        if (isAbsoluteActive()) {
+        if (!isAbsoluteActive()) {
+            // We lost focus, turn off the list.
+            if (list.isActive()) {
+                list.setEnabled(false);
+                list.setVisible(false);
+                setHeight(1);
+                if (limitToListValue == false) {
+                    activate(field);
+                }
+            }
+        }
+
+        if (isAbsoluteActive() && (limitToListValue == false)) {
             comboBoxColor = getTheme().getColor("tcombobox.active");
         } else {
             comboBoxColor = getTheme().getColor("tcombobox.inactive");
         }
 
-        getScreen().putCharXY(getWidth() - 1, 0, GraphicsChars.DOWNARROW,
+        putCharXY(getWidth() - 3, 0, GraphicsChars.DOWNARROWLEFT,
+            comboBoxColor);
+        putCharXY(getWidth() - 2, 0, GraphicsChars.DOWNARROW,
+            comboBoxColor);
+        putCharXY(getWidth() - 1, 0, GraphicsChars.DOWNARROWRIGHT,
             comboBoxColor);
     }
 
@@ -240,4 +292,33 @@ public class TComboBox extends TWidget {
         list.setSelectedIndex(-1);
     }
 
+    /**
+     * Set combobox text to one of the list values.
+     *
+     * @param index the index in the list
+     */
+    public void setIndex(final int index) {
+        list.setSelectedIndex(index);
+        field.setText(list.getSelected());
+    }
+
+    /**
+     * Get a copy of the list of strings to display.
+     *
+     * @return the list of strings
+     */
+    public final List<String> getList() {
+        return list.getList();
+    }
+
+    /**
+     * Set the new list of strings to display.
+     *
+     * @param list new list of strings
+     */
+    public final void setList(final List<String> list) {
+        this.list.setList(list);
+        field.setText("");
+    }
+
 }
index b6a0411c099018cf78d69690c8e5041c7c41a0fc..49b7e67122c738424254f894037639bc1392b646 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -170,11 +170,11 @@ public class TCommand {
     // ------------------------------------------------------------------------
 
     /**
-     * Protected constructor.  Subclasses can be used to define new commands.
+     * Public constructor.
      *
      * @param type the Type of command, one of EXIT, CASCADE, etc.
      */
-    protected TCommand(final int type) {
+    public TCommand(final int type) {
         this.type = type;
     }
 
index 23edfb8568bdff997ce4314a0c1dbe0df9802c71..20e6fd04c405e9edc918cdca4821264976929a4d 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index 9ff0b6bfde9ac1ec5e85fbb1a19a4179b5227a10..e00d14f696755d5a6756c7a031c0d8eda3cdca25 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -30,8 +30,9 @@ package jexer;
 
 import java.io.File;
 import java.util.ArrayList;
-import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 /**
  * TDirectoryList shows the files within a directory.
@@ -45,13 +46,18 @@ public class TDirectoryList extends TList {
     /**
      * Files in the directory.
      */
-    private List<File> files;
+    private Map<String, File> files;
 
     /**
      * Root path containing files to display.
      */
     private File path;
 
+    /**
+     * The list of filters that a file must match in order to be displayed.
+     */
+    private List<String> filters;
+
     // ------------------------------------------------------------------------
     // Constructors -----------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -69,7 +75,7 @@ public class TDirectoryList extends TList {
     public TDirectoryList(final TWidget parent, final String path, final int x,
         final int y, final int width, final int height) {
 
-        this(parent, path, x, y, width, height, null);
+        this(parent, path, x, y, width, height, null, null, null);
     }
 
     /**
@@ -81,13 +87,61 @@ public class TDirectoryList extends TList {
      * @param y row relative to parent
      * @param width width of text area
      * @param height height of text area
-     * @param action action to perform when an item is selected
+     * @param action action to perform when an item is selected (enter or
+     * double-click)
      */
     public TDirectoryList(final TWidget parent, final String path, final int x,
         final int y, final int width, final int height, final TAction action) {
 
+        this(parent, path, x, y, width, height, action, null, null);
+    }
+
+    /**
+     * Public constructor.
+     *
+     * @param parent parent widget
+     * @param path directory path, must be a directory
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param width width of text area
+     * @param height height of text area
+     * @param action action to perform when an item is selected (enter or
+     * double-click)
+     * @param singleClickAction action to perform when an item is selected
+     * (single-click)
+     */
+    public TDirectoryList(final TWidget parent, final String path, final int x,
+        final int y, final int width, final int height, final TAction action,
+        final TAction singleClickAction) {
+
+        this(parent, path, x, y, width, height, action, singleClickAction,
+            null);
+    }
+
+    /**
+     * Public constructor.
+     *
+     * @param parent parent widget
+     * @param path directory path, must be a directory
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param width width of text area
+     * @param height height of text area
+     * @param action action to perform when an item is selected (enter or
+     * double-click)
+     * @param singleClickAction action to perform when an item is selected
+     * (single-click)
+     * @param filters a list of strings that files must match to be displayed
+     */
+    public TDirectoryList(final TWidget parent, final String path, final int x,
+        final int y, final int width, final int height, final TAction action,
+        final TAction singleClickAction, final List<String> filters) {
+
         super(parent, null, x, y, width, height, action);
-        files = new ArrayList<File>();
+        files = new HashMap<String, File>();
+        this.filters = filters;
+        this.singleClickAction = singleClickAction;
+
         setPath(path);
     }
 
@@ -120,11 +174,29 @@ public class TDirectoryList extends TList {
                 if (newFiles[i].isDirectory()) {
                     continue;
                 }
-                files.add(newFiles[i]);
-                newStrings.add(renderFile(files.size() - 1));
+                if (filters != null) {
+                    for (String pattern: filters) {
+
+                        /*
+                        System.err.println("newFiles[i] " +
+                            newFiles[i].getName() + " " + pattern +
+                            " " + newFiles[i].getName().matches(pattern));
+                        */
+
+                        if (newFiles[i].getName().matches(pattern)) {
+                            String key = renderFile(newFiles[i]);
+                            files.put(key, newFiles[i]);
+                            newStrings.add(key);
+                            break;
+                        }
+                    }
+                } else {
+                    String key = renderFile(newFiles[i]);
+                    files.put(key, newFiles[i]);
+                    newStrings.add(key);
+                }
             }
         }
-        Collections.sort(newStrings);
         setList(newStrings);
 
         // Select the first entry
@@ -139,18 +211,17 @@ public class TDirectoryList extends TList {
      * @return the path
      */
     public File getPath() {
-        path = files.get(getSelectedIndex());
+        path = files.get(getSelected());
         return path;
     }
 
     /**
      * Format one of the entries for drawing on the screen.
      *
-     * @param index index into files
+     * @param file the File
      * @return the line to draw
      */
-    private String renderFile(final int index) {
-        File file = files.get(index);
+    private String renderFile(final File file) {
         String name = file.getName();
         if (name.length() > 20) {
             name = name.substring(0, 17) + "...";
index bc3712b7890b3d8d46be563b01d4cfb8bd172fa8..55be8aa61b18e45c50deba26720104ff04926e6a 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -219,16 +219,15 @@ public class TEditColorThemeWindow extends TWindow {
             CellAttributes background = getWindow().getBackground();
             CellAttributes attr = new CellAttributes();
 
-            getScreen().drawBox(0, 0, getWidth(), getHeight(), border,
-                background, 1, false);
+            drawBox(0, 0, getWidth(), getHeight(), border, background, 1,
+                false);
 
             attr.setTo(getTheme().getColor("twindow.background.modal"));
             if (isActive()) {
                 attr.setForeColor(getTheme().getColor("tlabel").getForeColor());
                 attr.setBold(getTheme().getColor("tlabel").isBold());
             }
-            getScreen().putStringXY(1, 0, i18n.getString("foregroundLabel"),
-                attr);
+            putStringXY(1, 0, i18n.getString("foregroundLabel"), attr);
 
             // Have to draw the colors manually because the int value matches
             // SGR, not CGA.
@@ -275,12 +274,11 @@ public class TEditColorThemeWindow extends TWindow {
                 // Use white-on-black for black.  All other colors use
                 // black-on-whatever.
                 attr.reset();
-                getScreen().putCharXY(dotX, dotY, GraphicsChars.CP437[0x07],
-                    attr);
+                putCharXY(dotX, dotY, GraphicsChars.CP437[0x07], attr);
             } else {
                 attr.setForeColor(color);
                 attr.setBold(bold);
-                getScreen().putCharXY(dotX, dotY, '\u25D8', attr);
+                putCharXY(dotX, dotY, '\u25D8', attr);
             }
         }
 
@@ -493,16 +491,15 @@ public class TEditColorThemeWindow extends TWindow {
             CellAttributes background = getWindow().getBackground();
             CellAttributes attr = new CellAttributes();
 
-            getScreen().drawBox(0, 0, getWidth(), getHeight(), border,
-                background, 1, false);
+            drawBox(0, 0, getWidth(), getHeight(), border, background, 1,
+                false);
 
             attr.setTo(getTheme().getColor("twindow.background.modal"));
             if (isActive()) {
                 attr.setForeColor(getTheme().getColor("tlabel").getForeColor());
                 attr.setBold(getTheme().getColor("tlabel").isBold());
             }
-            getScreen().putStringXY(1, 0, i18n.getString("backgroundLabel"),
-                attr);
+            putStringXY(1, 0, i18n.getString("backgroundLabel"), attr);
 
             // Have to draw the colors manually because the int value matches
             // SGR, not CGA.
@@ -531,11 +528,10 @@ public class TEditColorThemeWindow extends TWindow {
                 // Use white-on-black for black.  All other colors use
                 // black-on-whatever.
                 attr.reset();
-                getScreen().putCharXY(dotX, dotY, GraphicsChars.CP437[0x07],
-                    attr);
+                putCharXY(dotX, dotY, GraphicsChars.CP437[0x07], attr);
             } else {
                 attr.setForeColor(color);
-                getScreen().putCharXY(dotX, dotY, '\u25D8', attr);
+                putCharXY(dotX, dotY, '\u25D8', attr);
             }
 
         }
@@ -739,16 +735,16 @@ public class TEditColorThemeWindow extends TWindow {
             attr.setForeColor(getTheme().getColor("tlabel").getForeColor());
             attr.setBold(getTheme().getColor("tlabel").isBold());
         }
-        getScreen().putStringXY(3, 2, i18n.getString("colorName"), attr);
+        putStringXY(3, 2, i18n.getString("colorName"), attr);
 
         // Draw the sample text box
         attr.reset();
         attr.setForeColor(foreground.color);
         attr.setBold(foreground.bold);
         attr.setBackColor(background.color);
-        getScreen().putStringXY(getWidth() - 17, getHeight() - 6,
+        putStringXY(getWidth() - 17, getHeight() - 6,
             i18n.getString("textTextText"), attr);
-        getScreen().putStringXY(getWidth() - 17, getHeight() - 5,
+        putStringXY(getWidth() - 17, getHeight() - 5,
             i18n.getString("textTextText"), attr);
     }
 
index 94f1a3be545b2a401f8e32d2920faf3268c5fc1a..f65ba6b5823b4d1592288488b3e0f1aba4c036b6 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index 4a638fc8d0c2b3a3b293f787bca18f224d4aed38..495131106e7b9fad3fa38a1f3ddadf159238d776 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index aa0d18f05f174332c9191017502475150608a3ec..b4330b4cb3c809210c4abf07b6592b0ac69bbd20 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -358,9 +358,8 @@ public class TField extends TWidget {
         if (end > text.length()) {
             end = text.length();
         }
-        getScreen().hLineXY(0, 0, getWidth(), GraphicsChars.HATCH, fieldColor);
-        getScreen().putStringXY(0, 0, text.substring(windowStart, end),
-            fieldColor);
+        hLineXY(0, 0, getWidth(), GraphicsChars.HATCH, fieldColor);
+        putStringXY(0, 0, text.substring(windowStart, end), fieldColor);
 
         // Fix the cursor, it will be rendered by TApplication.drawAll().
         updateCursor();
@@ -385,6 +384,7 @@ public class TField extends TWidget {
      * @param text the new field text
      */
     public void setText(final String text) {
+        assert (text != null);
         this.text = text;
         position = 0;
         windowStart = 0;
index 6f46a034d6a70b394766460c71139ade0e293349..ac23cfd13557e58100bee15a9a878ecb55157b6d 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -30,8 +30,10 @@ package jexer;
 
 import java.io.File;
 import java.io.IOException;
+import java.util.List;
 import java.util.ResourceBundle;
 
+import jexer.backend.SwingTerminal;
 import jexer.bits.GraphicsChars;
 import jexer.event.TKeypressEvent;
 import jexer.ttree.TDirectoryTreeItem;
@@ -77,7 +79,12 @@ public class TFileOpenBox extends TWindow {
         /**
          * Button will be labeled "Save".
          */
-        SAVE
+        SAVE,
+
+        /**
+         * Button will be labeled "Select".
+         */
+        SELECT
     }
 
     // ------------------------------------------------------------------------
@@ -114,6 +121,11 @@ public class TFileOpenBox extends TWindow {
      */
     private TButton openButton;
 
+    /**
+     * The type of box this is (OPEN, SAVE, or SELECT).
+     */
+    private Type type = Type.OPEN;
+
     // ------------------------------------------------------------------------
     // Constructors -----------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -129,6 +141,21 @@ public class TFileOpenBox extends TWindow {
     public TFileOpenBox(final TApplication application, final String path,
         final Type type) throws IOException {
 
+        this(application, path, type, null);
+    }
+
+    /**
+     * Public constructor.  The file open box will be centered on screen.
+     *
+     * @param application the TApplication that manages this window
+     * @param path path of selected file
+     * @param type one of the Type constants
+     * @param filters a list of strings that files must match to be displayed
+     * @throws IOException of a java.io operation throws
+     */
+    public TFileOpenBox(final TApplication application, final String path,
+        final Type type, final List<String> filters) throws IOException {
+
         // Register with the TApplication
         super(application, "", 0, 0, 76, 22, MODAL);
 
@@ -140,7 +167,11 @@ public class TFileOpenBox extends TWindow {
                     try {
                         checkFilename(entryField.getText());
                     } catch (IOException e) {
-                        e.printStackTrace();
+                        // If the backend is Swing, we can emit the stack
+                        // trace to stderr.  Otherwise, just squash it.
+                        if (getScreen() instanceof SwingTerminal) {
+                            e.printStackTrace();
+                        }
                     }
                 }
             }, null);
@@ -154,10 +185,16 @@ public class TFileOpenBox extends TWindow {
                     File selectedDir = ((TDirectoryTreeItem) item).getFile();
                     try {
                         directoryList.setPath(selectedDir.getCanonicalPath());
-                        openButton.setEnabled(false);
+                        if (type == Type.OPEN) {
+                            openButton.setEnabled(false);
+                        }
                         activate(treeView);
                     } catch (IOException e) {
-                        e.printStackTrace();
+                        // If the backend is Swing, we can emit the stack
+                        // trace to stderr.  Otherwise, just squash it.
+                        if (getScreen() instanceof SwingTerminal) {
+                            e.printStackTrace();
+                        }
                     }
                 }
             }
@@ -174,12 +211,34 @@ public class TFileOpenBox extends TWindow {
                         entryField.onKeypress(new TKeypressEvent(kbEnd));
                         openButton.setEnabled(true);
                         activate(entryField);
+                        checkFilename(entryField.getText());
                     } catch (IOException e) {
-                        e.printStackTrace();
+                        // If the backend is Swing, we can emit the stack
+                        // trace to stderr.  Otherwise, just squash it.
+                        if (getScreen() instanceof SwingTerminal) {
+                            e.printStackTrace();
+                        }
                     }
                 }
-            }
-        );
+            },
+            new TAction() {
+                public void DO() {
+                    try {
+                        File newPath = directoryList.getPath();
+                        entryField.setText(newPath.getCanonicalPath());
+                        entryField.onKeypress(new TKeypressEvent(kbEnd));
+                        openButton.setEnabled(true);
+                        activate(entryField);
+                    } catch (IOException e) {
+                        // If the backend is Swing, we can emit the stack
+                        // trace to stderr.  Otherwise, just squash it.
+                        if (getScreen() instanceof SwingTerminal) {
+                            e.printStackTrace();
+                        }
+                    }
+                }
+            },
+            filters);
 
         String openLabel = "";
         switch (type) {
@@ -191,9 +250,14 @@ public class TFileOpenBox extends TWindow {
             openLabel = i18n.getString("saveButton");
             setTitle(i18n.getString("saveTitle"));
             break;
+        case SELECT:
+            openLabel = i18n.getString("selectButton");
+            setTitle(i18n.getString("selectTitle"));
+            break;
         default:
             throw new IllegalArgumentException("Invalid type: " + type);
         }
+        this.type = type;
 
         // Setup button actions
         openButton = addButton(openLabel, this.getWidth() - 12, 3,
@@ -202,12 +266,18 @@ public class TFileOpenBox extends TWindow {
                     try {
                         checkFilename(entryField.getText());
                     } catch (IOException e) {
-                        e.printStackTrace();
+                        // If the backend is Swing, we can emit the stack
+                        // trace to stderr.  Otherwise, just squash it.
+                        if (getScreen() instanceof SwingTerminal) {
+                            e.printStackTrace();
+                        }
                     }
                 }
             }
         );
-        openButton.setEnabled(false);
+        if (type == Type.OPEN) {
+            openButton.setEnabled(false);
+        }
 
         addButton(i18n.getString("cancelButton"), getWidth() - 12, 5,
             new TAction() {
@@ -265,10 +335,16 @@ public class TFileOpenBox extends TWindow {
                 File selectedDir = ((TDirectoryTreeItem) item).getFile();
                 try {
                     directoryList.setPath(selectedDir.getCanonicalPath());
-                    openButton.setEnabled(false);
+                    if (type == Type.OPEN) {
+                        openButton.setEnabled(false);
+                    }
                     activate(treeView);
                 } catch (IOException e) {
-                    e.printStackTrace();
+                    // If the backend is Swing, we can emit the stack trace
+                    // to stderr.  Otherwise, just squash it.
+                    if (getScreen() instanceof SwingTerminal) {
+                        e.printStackTrace();
+                    }
                 }
                 return;
             }
@@ -288,7 +364,7 @@ public class TFileOpenBox extends TWindow {
     @Override
     public void draw() {
         super.draw();
-        getScreen().vLineXY(33, 4, getHeight() - 6, GraphicsChars.WINDOW_SIDE,
+        vLineXY(33, 4, getHeight() - 6, GraphicsChars.WINDOW_SIDE,
             getBackground());
     }
 
@@ -324,9 +400,15 @@ public class TFileOpenBox extends TWindow {
                 treeViewRoot = new TDirectoryTreeItem(treeView,
                     newFilename, true);
                 treeView.setTreeRoot(treeViewRoot, true);
-                openButton.setEnabled(false);
+                if (type == Type.OPEN) {
+                    openButton.setEnabled(false);
+                }
                 directoryList.setPath(newFilename);
             }
+        } else if (type != Type.OPEN) {
+            filename = newFilename;
+            getApplication().closeWindow(this);
+            return;
         }
     }
 
index 1dee73881dfeae52d2c215517b0297b35bf38ee8..ef40e8644a46d1a3e83cd82ab8792ccb8de7e455 100644 (file)
@@ -3,3 +3,5 @@ openTitle=Open File...
 saveButton=\ &Save\ 
 saveTitle=Save File...
 cancelButton=&Cancel
+selectButton=S&elect
+selectTitle=Select File...
index eab1a15cc525bdc1e05a2fcc89b846f24bb3a46b..a07bcd7a52b7636949e57948892d395cba41bcf9 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -216,19 +216,15 @@ public class THScroller extends TWidget {
     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);
+        putCharXY(0, 0, GraphicsChars.CP437[0x11], arrowColor);
+        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);
+            hLineXY(1, 0, getWidth() - 2, GraphicsChars.CP437[0xB1], barColor);
+            putCharXY(boxPosition(), 0, GraphicsChars.BOX, arrowColor);
         } else {
-            getScreen().hLineXY(1, 0, getWidth() - 2, GraphicsChars.HATCH,
-                barColor);
+            hLineXY(1, 0, getWidth() - 2, GraphicsChars.HATCH, barColor);
         }
 
     }
diff --git a/src/jexer/TImage.java b/src/jexer/TImage.java
new file mode 100644 (file)
index 0000000..1a9aa9e
--- /dev/null
@@ -0,0 +1,499 @@
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer;
+
+import java.awt.image.BufferedImage;
+
+import jexer.backend.ECMA48Terminal;
+import jexer.backend.MultiScreen;
+import jexer.backend.SwingTerminal;
+import jexer.bits.Cell;
+import jexer.event.TKeypressEvent;
+import jexer.event.TMouseEvent;
+import static jexer.TKeypress.*;
+
+/**
+ * TImage renders a piece of a bitmap image on screen.
+ */
+public class TImage extends TWidget {
+
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * The action to perform when the user clicks on the image.
+     */
+    private TAction clickAction;
+
+    /**
+     * The image to display.
+     */
+    private BufferedImage image;
+
+    /**
+     * The original image from construction time.
+     */
+    private BufferedImage originalImage;
+
+    /**
+     * The current scaling factor for the image.
+     */
+    private double scaleFactor = 1.0;
+
+    /**
+     * The current clockwise rotation for the image.
+     */
+    private int clockwise = 0;
+
+    /**
+     * Left column of the image.  0 is the left-most column.
+     */
+    private int left;
+
+    /**
+     * Top row of the image.  0 is the top-most row.
+     */
+    private int top;
+
+    /**
+     * The cells containing the broken up image pieces.
+     */
+    private Cell cells[][];
+
+    /**
+     * The number of rows in cells[].
+     */
+    private int cellRows;
+
+    /**
+     * The number of columns in cells[].
+     */
+    private int cellColumns;
+
+    /**
+     * Last text width value.
+     */
+    private int lastTextWidth = -1;
+
+    /**
+     * Last text height value.
+     */
+    private int lastTextHeight = -1;
+
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Public constructor.
+     *
+     * @param parent parent widget
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param width number of text cells for width of the image
+     * @param height number of text cells for height of the image
+     * @param image the image to display
+     * @param left left column of the image.  0 is the left-most column.
+     * @param top top row of the image.  0 is the top-most row.
+     * @param clickAction function to call when mouse is pressed
+     */
+    public TImage(final TWidget parent, final int x, final int y,
+        final int width, final int height,
+        final BufferedImage image, final int left, final int top,
+        final TAction clickAction) {
+
+        // Set parent and window
+        super(parent, x, y, width, height);
+
+        setCursorVisible(false);
+        this.originalImage = image;
+        this.left = left;
+        this.top = top;
+        this.clickAction = clickAction;
+
+        sizeToImage(true);
+
+        getApplication().addImage(this);
+    }
+
+    // ------------------------------------------------------------------------
+    // Event handlers ---------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Subclasses should override this method to cleanup resources.  This is
+     * called by TWindow.onClose().
+     */
+    @Override
+    protected void close() {
+        getApplication().removeImage(this);
+        super.close();
+    }
+
+    /**
+     * Handle mouse press events.
+     *
+     * @param mouse mouse button press event
+     */
+    @Override
+    public void onMouseDown(final TMouseEvent mouse) {
+        if (clickAction != null) {
+            clickAction.DO();
+            return;
+        }
+    }
+
+    /**
+     * Handle keystrokes.
+     *
+     * @param keypress keystroke event
+     */
+    @Override
+    public void onKeypress(final TKeypressEvent keypress) {
+        if (!keypress.getKey().isFnKey()) {
+            if (keypress.getKey().getChar() == '+') {
+                // Make the image bigger.
+                scaleFactor *= 1.25;
+                image = null;
+                sizeToImage(true);
+                return;
+            }
+            if (keypress.getKey().getChar() == '-') {
+                // Make the image smaller.
+                scaleFactor *= 0.80;
+                image = null;
+                sizeToImage(true);
+                return;
+            }
+        }
+        if (keypress.equals(kbAltUp)) {
+            // Make the image bigger.
+            scaleFactor *= 1.25;
+            image = null;
+            sizeToImage(true);
+            return;
+        }
+        if (keypress.equals(kbAltDown)) {
+            // Make the image smaller.
+            scaleFactor *= 0.80;
+            image = null;
+            sizeToImage(true);
+            return;
+        }
+        if (keypress.equals(kbAltRight)) {
+            // Rotate clockwise.
+            clockwise++;
+            clockwise %= 4;
+            image = null;
+            sizeToImage(true);
+            return;
+        }
+        if (keypress.equals(kbAltLeft)) {
+            // Rotate counter-clockwise.
+            clockwise--;
+            if (clockwise < 0) {
+                clockwise = 3;
+            }
+            image = null;
+            sizeToImage(true);
+            return;
+        }
+
+        // Pass to parent for the things we don't care about.
+        super.onKeypress(keypress);
+    }
+
+    // ------------------------------------------------------------------------
+    // TWidget ----------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Draw the image.
+     */
+    @Override
+    public void draw() {
+        sizeToImage(false);
+
+        // We have already broken the image up, just draw the last set of
+        // cells.
+        for (int x = 0; (x < getWidth()) && (x + left < cellColumns); x++) {
+            if ((left + x) * lastTextWidth > image.getWidth()) {
+                continue;
+            }
+
+            for (int y = 0; (y < getHeight()) && (y + top < cellRows); y++) {
+                if ((top + y) * lastTextHeight > image.getHeight()) {
+                    continue;
+                }
+                assert (x + left < cellColumns);
+                assert (y + top < cellRows);
+
+                getWindow().putCharXY(x, y, cells[x + left][y + top]);
+            }
+        }
+
+    }
+
+    // ------------------------------------------------------------------------
+    // TImage -----------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Size cells[][] according to the screen font size.
+     *
+     * @param always if true, always resize the cells
+     */
+    private void sizeToImage(final boolean always) {
+        int textWidth = 16;
+        int textHeight = 20;
+
+        if (getScreen() instanceof SwingTerminal) {
+            SwingTerminal terminal = (SwingTerminal) getScreen();
+
+            textWidth = terminal.getTextWidth();
+            textHeight = terminal.getTextHeight();
+        } if (getScreen() instanceof MultiScreen) {
+            MultiScreen terminal = (MultiScreen) getScreen();
+
+            textWidth = terminal.getTextWidth();
+            textHeight = terminal.getTextHeight();
+        } else if (getScreen() instanceof ECMA48Terminal) {
+            ECMA48Terminal terminal = (ECMA48Terminal) getScreen();
+
+            textWidth = terminal.getTextWidth();
+            textHeight = terminal.getTextHeight();
+        }
+
+        if (image == null) {
+            image = scaleImage(originalImage, scaleFactor);
+            image = rotateImage(image, clockwise);
+        }
+
+        if ((always == true) ||
+            ((textWidth > 0)
+                && (textWidth != lastTextWidth)
+                && (textHeight > 0)
+                && (textHeight != lastTextHeight))
+        ) {
+            cellColumns = image.getWidth() / textWidth;
+            if (cellColumns * textWidth < image.getWidth()) {
+                cellColumns++;
+            }
+            cellRows = image.getHeight() / textHeight;
+            if (cellRows * textHeight < image.getHeight()) {
+                cellRows++;
+            }
+
+            // Break the image up into an array of cells.
+            cells = new Cell[cellColumns][cellRows];
+
+            for (int x = 0; x < cellColumns; x++) {
+                for (int y = 0; y < cellRows; y++) {
+
+                    int width = textWidth;
+                    if ((x + 1) * textWidth > image.getWidth()) {
+                        width = image.getWidth() - (x * textWidth);
+                    }
+                    int height = textHeight;
+                    if ((y + 1) * textHeight > image.getHeight()) {
+                        height = image.getHeight() - (y * textHeight);
+                    }
+
+                    Cell cell = new Cell();
+                    cell.setImage(image.getSubimage(x * textWidth,
+                            y * textHeight, width, height));
+
+                    cells[x][y] = cell;
+                }
+            }
+
+            lastTextWidth = textWidth;
+            lastTextHeight = textHeight;
+        }
+
+        if ((left + getWidth()) > cellColumns) {
+            left = cellColumns - getWidth();
+        }
+        if (left < 0) {
+            left = 0;
+        }
+        if ((top + getHeight()) > cellRows) {
+            top = cellRows - getHeight();
+        }
+        if (top < 0) {
+            top = 0;
+        }
+    }
+
+    /**
+     * Get the top corner to render.
+     *
+     * @return the top row
+     */
+    public int getTop() {
+        return top;
+    }
+
+    /**
+     * Set the top corner to render.
+     *
+     * @param top the new top row
+     */
+    public void setTop(final int top) {
+        this.top = top;
+        if (this.top > cellRows - getHeight()) {
+            this.top = cellRows - getHeight();
+        }
+        if (this.top < 0) {
+            this.top = 0;
+        }
+    }
+
+    /**
+     * Get the left corner to render.
+     *
+     * @return the left column
+     */
+    public int getLeft() {
+        return left;
+    }
+
+    /**
+     * Set the left corner to render.
+     *
+     * @param left the new left column
+     */
+    public void setLeft(final int left) {
+        this.left = left;
+        if (this.left > cellColumns - getWidth()) {
+            this.left = cellColumns - getWidth();
+        }
+        if (this.left < 0) {
+            this.left = 0;
+        }
+    }
+
+    /**
+     * Get the number of text cell rows for this image.
+     *
+     * @return the number of rows
+     */
+    public int getRows() {
+        return cellRows;
+    }
+
+    /**
+     * Get the number of text cell columns for this image.
+     *
+     * @return the number of columns
+     */
+    public int getColumns() {
+        return cellColumns;
+    }
+
+    /**
+     * Scale an image by to be scaleFactor size.
+     *
+     * @param image the image to scale
+     * @param factor the scale to make the new image
+     */
+    private BufferedImage scaleImage(final BufferedImage image,
+        final double factor) {
+
+        if (Math.abs(factor - 1.0) < 0.03) {
+            // If we are within 3% of 1.0, just return the original image.
+            return image;
+        }
+
+        int width = (int) (image.getWidth() * factor);
+        int height = (int) (image.getHeight() * factor);
+
+        BufferedImage newImage = new BufferedImage(width, height,
+            BufferedImage.TYPE_INT_ARGB);
+
+        java.awt.Graphics gr = newImage.createGraphics();
+        gr.drawImage(image, 0, 0, width, height, null);
+        gr.dispose();
+
+        return newImage;
+    }
+
+    /**
+     * Rotate an image either clockwise or counterclockwise.
+     *
+     * @param image the image to scale
+     * @param clockwise number of turns clockwise
+     */
+    private BufferedImage rotateImage(final BufferedImage image,
+        final int clockwise) {
+
+        if (clockwise % 4 == 0) {
+            return image;
+        }
+
+        BufferedImage newImage = null;
+
+        if (clockwise % 4 == 1) {
+            // 90 degrees clockwise
+            newImage = new BufferedImage(image.getHeight(), image.getWidth(),
+                BufferedImage.TYPE_INT_ARGB);
+            for (int x = 0; x < image.getWidth(); x++) {
+                for (int y = 0; y < image.getHeight(); y++) {
+                    newImage.setRGB(y, x,
+                        image.getRGB(x, image.getHeight() - 1 - y));
+                }
+            }
+        } else if (clockwise % 4 == 2) {
+            // 180 degrees clockwise
+            newImage = new BufferedImage(image.getWidth(), image.getHeight(),
+                BufferedImage.TYPE_INT_ARGB);
+            for (int x = 0; x < image.getWidth(); x++) {
+                for (int y = 0; y < image.getHeight(); y++) {
+                    newImage.setRGB(x, y,
+                        image.getRGB(image.getWidth() - 1 - x,
+                            image.getHeight() - 1 - y));
+                }
+            }
+        } else if (clockwise % 4 == 3) {
+            // 270 degrees clockwise
+            newImage = new BufferedImage(image.getHeight(), image.getWidth(),
+                BufferedImage.TYPE_INT_ARGB);
+            for (int x = 0; x < image.getWidth(); x++) {
+                for (int y = 0; y < image.getHeight(); y++) {
+                    newImage.setRGB(y, x,
+                        image.getRGB(image.getWidth() - 1 - x, y));
+                }
+            }
+        }
+
+        return newImage;
+    }
+
+}
diff --git a/src/jexer/TImageWindow.java b/src/jexer/TImageWindow.java
new file mode 100644 (file)
index 0000000..d38cd25
--- /dev/null
@@ -0,0 +1,280 @@
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer;
+
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import javax.imageio.ImageIO;
+
+import jexer.event.TKeypressEvent;
+import jexer.event.TMouseEvent;
+import jexer.event.TResizeEvent;
+import static jexer.TKeypress.*;
+
+/**
+ * TImageWindow shows an image with scrollbars.
+ */
+public class TImageWindow extends TScrollableWindow {
+
+    // ------------------------------------------------------------------------
+    // Constants --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * The number of lines to scroll on mouse wheel up/down.
+     */
+    private static final int wheelScrollSize = 3;
+
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Hang onto the TImage so I can resize it with the window.
+     */
+    private TImage imageField;
+
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Public constructor opens a file.
+     *
+     * @param parent the main application
+     * @param file the file to open
+     * @throws IOException if a java.io operation throws
+     */
+    public TImageWindow(final TApplication parent,
+        final File file) throws IOException {
+
+        this(parent, file, 0, 0, parent.getScreen().getWidth(),
+            parent.getScreen().getHeight() - 2);
+    }
+
+    /**
+     * Public constructor opens a file.
+     *
+     * @param parent the main application
+     * @param file the file to open
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param width width of window
+     * @param height height of window
+     * @throws IOException if a java.io operation throws
+     */
+    public TImageWindow(final TApplication parent, final File file,
+        final int x, final int y, final int width,
+        final int height) throws IOException {
+
+        super(parent, file.getName(), x, y, width, height, RESIZABLE);
+
+        BufferedImage image = ImageIO.read(file);
+
+        imageField = new TImage(this, 0, 0, getWidth() - 2, getHeight() - 2,
+            image, 0, 0, null);
+        setTitle(file.getName());
+
+        setupAfterImage();
+    }
+
+    /**
+     * Setup other fields after the image is created.
+     */
+    private void setupAfterImage() {
+        if (imageField.getRows() < getHeight() - 2) {
+            imageField.setHeight(imageField.getRows());
+            setHeight(imageField.getRows() + 2);
+        }
+        if (imageField.getColumns() < getWidth() - 2) {
+            imageField.setWidth(imageField.getColumns());
+            setWidth(imageField.getColumns() + 2);
+        }
+
+        hScroller = new THScroller(this, 17, getHeight() - 2, getWidth() - 20);
+        vScroller = new TVScroller(this, getWidth() - 2, 0, getHeight() - 2);
+        setTopValue(0);
+        setBottomValue(imageField.getRows() - imageField.getHeight());
+        setLeftValue(0);
+        setRightValue(imageField.getColumns() - imageField.getWidth());
+    }
+
+    // ------------------------------------------------------------------------
+    // Event handlers ---------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Handle mouse press events.
+     *
+     * @param mouse mouse button press event
+     */
+    @Override
+    public void onMouseDown(final TMouseEvent mouse) {
+        // Use TWidget's code to pass the event to the children.
+        super.onMouseDown(mouse);
+
+        if (mouse.isMouseWheelUp()) {
+            imageField.setTop(imageField.getTop() - wheelScrollSize);
+        } else if (mouse.isMouseWheelDown()) {
+            imageField.setTop(imageField.getTop() + wheelScrollSize);
+        }
+        setVerticalValue(imageField.getTop());
+    }
+
+    /**
+     * Handle mouse release events.
+     *
+     * @param mouse mouse button release event
+     */
+    @Override
+    public void onMouseUp(final TMouseEvent mouse) {
+        // Use TWidget's code to pass the event to the children.
+        super.onMouseUp(mouse);
+
+        if (mouse.isMouse1() && mouseOnVerticalScroller(mouse)) {
+            // Clicked/dragged on vertical scrollbar
+            imageField.setTop(getVerticalValue());
+        }
+        if (mouse.isMouse1() && mouseOnHorizontalScroller(mouse)) {
+            // Clicked/dragged on horizontal scrollbar
+            imageField.setLeft(getHorizontalValue());
+        }
+    }
+
+    /**
+     * Method that subclasses can override to handle mouse movements.
+     *
+     * @param mouse mouse motion event
+     */
+    @Override
+    public void onMouseMotion(final TMouseEvent mouse) {
+        // Use TWidget's code to pass the event to the children.
+        super.onMouseMotion(mouse);
+
+        if (mouse.isMouse1() && mouseOnVerticalScroller(mouse)) {
+            // Clicked/dragged on vertical scrollbar
+            imageField.setTop(getVerticalValue());
+        }
+        if (mouse.isMouse1() && mouseOnHorizontalScroller(mouse)) {
+            // Clicked/dragged on horizontal scrollbar
+            imageField.setLeft(getHorizontalValue());
+        }
+    }
+
+    /**
+     * Handle window/screen resize events.
+     *
+     * @param event resize event
+     */
+    @Override
+    public void onResize(final TResizeEvent event) {
+        if (event.getType() == TResizeEvent.Type.WIDGET) {
+            // Resize the image field
+            TResizeEvent imageSize = new TResizeEvent(TResizeEvent.Type.WIDGET,
+                event.getWidth() - 2, event.getHeight() - 2);
+            imageField.onResize(imageSize);
+
+            // Have TScrollableWindow handle the scrollbars
+            super.onResize(event);
+            return;
+        }
+
+        // Pass to children instead
+        for (TWidget widget: getChildren()) {
+            widget.onResize(event);
+        }
+    }
+
+    /**
+     * Handle keystrokes.
+     *
+     * @param keypress keystroke event
+     */
+    @Override
+    public void onKeypress(final TKeypressEvent keypress) {
+        if (keypress.equals(kbUp)) {
+            verticalDecrement();
+            imageField.setTop(getVerticalValue());
+            return;
+        }
+        if (keypress.equals(kbDown)) {
+            verticalIncrement();
+            imageField.setTop(getVerticalValue());
+            return;
+        }
+        if (keypress.equals(kbPgUp)) {
+            bigVerticalDecrement();
+            imageField.setTop(getVerticalValue());
+            return;
+        }
+        if (keypress.equals(kbPgDn)) {
+            bigVerticalIncrement();
+            imageField.setTop(getVerticalValue());
+            return;
+        }
+        if (keypress.equals(kbRight)) {
+            horizontalIncrement();
+            imageField.setLeft(getHorizontalValue());
+            return;
+        }
+        if (keypress.equals(kbLeft)) {
+            horizontalDecrement();
+            imageField.setLeft(getHorizontalValue());
+            return;
+        }
+
+        // We did not take it, let the TImage instance see it.
+        super.onKeypress(keypress);
+
+        setVerticalValue(imageField.getTop());
+        setBottomValue(imageField.getRows() - imageField.getHeight());
+        setHorizontalValue(imageField.getLeft());
+        setRightValue(imageField.getColumns() - imageField.getWidth());
+    }
+
+    // ------------------------------------------------------------------------
+    // TWindow ----------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Draw the window.
+     */
+    @Override
+    public void draw() {
+        // Draw as normal.
+        super.draw();
+
+        // We have to get the scrollbar values after we have let the image
+        // try to draw.
+        setBottomValue(imageField.getRows() - imageField.getHeight());
+        setRightValue(imageField.getColumns() - imageField.getWidth());
+    }
+
+}
index 487664ab01af2dac599282f3a9fedb46e03ee330..813af8823db263e5274090796e02fbf61ec958a6 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -110,17 +110,20 @@ public class TInputBox extends TMessageBox {
         setHeight(getHeight() + 2);
         field = addField(1, getHeight() - 6, getWidth() - 4, false, text);
 
+        // Set the secondaryThread to run me
+        getApplication().enableSecondaryEventReceiver(this);
+
         // Yield to the secondary thread.  When I come back from the
         // constructor response will already be set.
         getApplication().yield();
     }
 
     // ------------------------------------------------------------------------
-    // TInputBox --------------------------------------------------------------
+    // TMessageBox ------------------------------------------------------------
     // ------------------------------------------------------------------------
 
     // ------------------------------------------------------------------------
-    // TMessageBox ------------------------------------------------------------
+    // TInputBox --------------------------------------------------------------
     // ------------------------------------------------------------------------
 
     /**
index e0aa3b34a7e6243c6752d66bf7b5970886f76832..58085455eda4897a6ed3c1ee3f045fd11cf7f6c6 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -796,6 +796,11 @@ public class TKeypress {
      */
     @Override
     public String toString() {
+        // Special case: Enter is "<arrow> <line> <angle>"
+        if (equals(kbEnter)) {
+            return "\u25C0\u2500\u2518";
+        }
+
         if (isFunctionKey) {
             switch (keyCode) {
             case F1:
index 71530f0c33b4f44c94155d212f28f94f7dca1bd9..69e1efd22b01024e6ca3348c87f325197bd4b1a9 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -124,7 +124,7 @@ public class TLabel extends TWidget {
             CellAttributes background = getWindow().getBackground();
             color.setBackColor(background.getBackColor());
         }
-        getScreen().putStringXY(0, 0, label, color);
+        putStringXY(0, 0, label, color);
     }
 
     // ------------------------------------------------------------------------
index 46c9307b95eba5cf9f68ed59d82ed4893ca7f1f2..da60af18745737dbc5ed5ecaf7d3a9a08730f430 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -29,6 +29,7 @@
 package jexer;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 import jexer.bits.CellAttributes;
@@ -61,14 +62,20 @@ public class TList extends TScrollableWidget {
     private int maxLineWidth;
 
     /**
-     * The action to perform when the user selects an item (clicks or enter).
+     * The action to perform when the user selects an item (double-clicks or
+     * enter).
      */
-    private TAction enterAction = null;
+    protected TAction enterAction = null;
+
+    /**
+     * The action to perform when the user selects an item (single-click).
+     */
+    protected TAction singleClickAction = null;
 
     /**
      * The action to perform when the user navigates with keyboard.
      */
-    private TAction moveAction = null;
+    protected TAction moveAction = null;
 
     // ------------------------------------------------------------------------
     // Constructors -----------------------------------------------------------
@@ -170,9 +177,11 @@ public class TList extends TScrollableWidget {
         }
 
         if ((mouse.getX() < getWidth() - 1)
-            && (mouse.getY() < getHeight() - 1)) {
+            && (mouse.getY() < getHeight() - 1)
+        ) {
             if (getVerticalValue() + mouse.getY() < strings.size()) {
                 selectedString = getVerticalValue() + mouse.getY();
+                dispatchSingleClick();
             }
             return;
         }
@@ -189,7 +198,8 @@ public class TList extends TScrollableWidget {
     @Override
     public void onMouseDoubleClick(final TMouseEvent mouse) {
         if ((mouse.getX() < getWidth() - 1)
-            && (mouse.getY() < getHeight() - 1)) {
+            && (mouse.getY() < getHeight() - 1)
+        ) {
             if (getVerticalValue() + mouse.getY() < strings.size()) {
                 selectedString = getVerticalValue() + mouse.getY();
                 dispatchEnter();
@@ -329,7 +339,7 @@ public class TList extends TScrollableWidget {
     }
 
     /**
-     * Draw the files list.
+     * Draw the list.
      */
     @Override
     public void draw() {
@@ -355,8 +365,7 @@ public class TList extends TScrollableWidget {
                 color = getTheme().getColor("tlist.inactive");
             }
             String formatString = "%-" + Integer.toString(getWidth() - 1) + "s";
-            getScreen().putStringXY(0, topY, String.format(formatString, line),
-                    color);
+            putStringXY(0, topY, String.format(formatString, line), color);
             topY++;
             if (topY >= getHeight() - 1) {
                 break;
@@ -371,7 +380,7 @@ public class TList extends TScrollableWidget {
 
         // Pad the rest with blank lines
         for (int i = topY; i < getHeight() - 1; i++) {
-            getScreen().hLineXY(0, i, getWidth() - 1, ' ', color);
+            hLineXY(0, i, getWidth() - 1, ' ', color);
         }
     }
 
@@ -418,6 +427,15 @@ public class TList extends TScrollableWidget {
         return strings.size() - 1;
     }
 
+    /**
+     * Get a copy of the list of strings to display.
+     *
+     * @return the list of strings
+     */
+    public final List<String> getList() {
+        return new ArrayList<String>(strings);
+    }
+
     /**
      * Set the new list of strings to display.
      *
@@ -451,4 +469,15 @@ public class TList extends TScrollableWidget {
         }
     }
 
+    /**
+     * Perform single-click action.
+     */
+    public void dispatchSingleClick() {
+        assert (selectedString >= 0);
+        assert (selectedString < strings.size());
+        if (singleClickAction != null) {
+            singleClickAction.DO();
+        }
+    }
+
 }
index 6104d9f9783525d640fa74641f0e6d8f9efa6b27..1eff8a7613ef120ef1b88428931d60fa18d7fc48 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -178,7 +178,7 @@ public class TMessageBox extends TWindow {
     protected TMessageBox(final TApplication application, final String title,
         final String caption, final Type type, final boolean yield) {
 
-        // Start as 50x50 at (1, 1).  These will be changed later.
+        // Start as 100x100 at (1, 1).  These will be changed later.
         super(application, title, 1, 1, 100, 100, CENTERED | MODAL);
 
         // Hang onto type so that we can provide more convenience in
@@ -327,13 +327,14 @@ public class TMessageBox extends TWindow {
             break;
 
         default:
-            throw new IllegalArgumentException("Invalid message box type: " + type);
+            throw new IllegalArgumentException("Invalid message box type: " +
+                type);
         }
 
-        // Set the secondaryThread to run me
-        getApplication().enableSecondaryEventReceiver(this);
-
         if (yield) {
+            // Set the secondaryThread to run me
+            getApplication().enableSecondaryEventReceiver(this);
+
             // Yield to the secondary thread.  When I come back from the
             // constructor response will already be set.
             getApplication().yield();
@@ -422,4 +423,40 @@ public class TMessageBox extends TWindow {
         return result;
     }
 
+    /**
+     * See if the user clicked YES.
+     *
+     * @return true if the user clicked YES
+     */
+    public final boolean isYes() {
+        return (result == Result.YES);
+    }
+
+    /**
+     * See if the user clicked NO.
+     *
+     * @return true if the user clicked NO
+     */
+    public final boolean isNo() {
+        return (result == Result.NO);
+    }
+
+    /**
+     * See if the user clicked OK.
+     *
+     * @return true if the user clicked OK
+     */
+    public final boolean isOk() {
+        return (result == Result.OK);
+    }
+
+    /**
+     * See if the user clicked CANCEL.
+     *
+     * @return true if the user clicked CANCEL
+     */
+    public final boolean isCancel() {
+        return (result == Result.CANCEL);
+    }
+
 }
index 696598b94339ac77935b6ff5a51f018244ca6c25..ea88dfa45f96761e868fa65236895fb8f22dde98 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -115,13 +115,11 @@ public class TPasswordField extends TField {
             end = text.length();
         }
 
-        getScreen().hLineXY(0, 0, getWidth(), GraphicsChars.HATCH, fieldColor);
+        hLineXY(0, 0, getWidth(), GraphicsChars.HATCH, fieldColor);
         if (showStars) {
-            getScreen().hLineXY(0, 0, getWidth() - 2, '*',
-                fieldColor);
+            hLineXY(0, 0, getWidth() - 2, '*', fieldColor);
         } else {
-            getScreen().putStringXY(0, 0, text.substring(windowStart, end),
-                fieldColor);
+            putStringXY(0, 0, text.substring(windowStart, end), fieldColor);
         }
 
         // Fix the cursor, it will be rendered by TApplication.drawAll().
index 3947a940af7c28710988361272dd4d74c839912b..b144fd2757fddda34aa620698e40c3eed3bba2da 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -98,26 +98,23 @@ public class TProgressBar extends TWidget {
         int progressInt = (int)(progress * 100);
         int progressUnit = 100 / (getWidth() - 2);
 
-        getScreen().putCharXY(0, 0, GraphicsChars.CP437[0xC3], incompleteColor);
+        putCharXY(0, 0, GraphicsChars.CP437[0xC3], incompleteColor);
         for (int i = 0; i < getWidth() - 2; i++) {
             float iProgress = (float)i / (getWidth() - 2);
             int iProgressInt = (int)(iProgress * 100);
             if (iProgressInt <= progressInt - progressUnit) {
-                getScreen().putCharXY(i + 1, 0, GraphicsChars.BOX,
-                    completeColor);
+                putCharXY(i + 1, 0, GraphicsChars.BOX, completeColor);
             } else {
-                getScreen().putCharXY(i + 1, 0, GraphicsChars.SINGLE_BAR,
-                    incompleteColor);
+                putCharXY(i + 1, 0, GraphicsChars.SINGLE_BAR, incompleteColor);
             }
         }
         if (value >= maxValue) {
-            getScreen().putCharXY(getWidth() - 2, 0, GraphicsChars.BOX,
-                completeColor);
+            putCharXY(getWidth() - 2, 0, GraphicsChars.BOX, completeColor);
         } else {
-            getScreen().putCharXY(getWidth() - 2, 0, GraphicsChars.SINGLE_BAR,
+            putCharXY(getWidth() - 2, 0, GraphicsChars.SINGLE_BAR,
                 incompleteColor);
         }
-        getScreen().putCharXY(getWidth() - 1, 0, GraphicsChars.CP437[0xB4],
+        putCharXY(getWidth() - 1, 0, GraphicsChars.CP437[0xB4],
             incompleteColor);
     }
 
index da077037daec96a0474406fc9158a47a76072a58..b4170ba82cbd579494e2967f0055e5ecedfe09ac 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -158,15 +158,14 @@ public class TRadioButton extends TWidget {
             radioButtonColor = getTheme().getColor("tradiobutton.inactive");
         }
 
-        getScreen().putCharXY(0, 0, '(', radioButtonColor);
+        putCharXY(0, 0, '(', radioButtonColor);
         if (selected) {
-            getScreen().putCharXY(1, 0, GraphicsChars.CP437[0x07],
-                radioButtonColor);
+            putCharXY(1, 0, GraphicsChars.CP437[0x07], radioButtonColor);
         } else {
-            getScreen().putCharXY(1, 0, ' ', radioButtonColor);
+            putCharXY(1, 0, ' ', radioButtonColor);
         }
-        getScreen().putCharXY(2, 0, ')', radioButtonColor);
-        getScreen().putStringXY(4, 0, label, radioButtonColor);
+        putCharXY(2, 0, ')', radioButtonColor);
+        putStringXY(4, 0, label, radioButtonColor);
     }
 
     // ------------------------------------------------------------------------
index 6e6f39d22ad0f955f4b78bd3c9ffa140077828e3..d57d86478a8ed502d1437bc5a74fca9125747c67 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -87,11 +87,11 @@ public class TRadioGroup extends TWidget {
             radioGroupColor = getTheme().getColor("tradiogroup.inactive");
         }
 
-        getScreen().drawBox(0, 0, getWidth(), getHeight(),
-            radioGroupColor, radioGroupColor, 3, false);
+        drawBox(0, 0, getWidth(), getHeight(), radioGroupColor, radioGroupColor,
+            3, false);
 
-        getScreen().hLineXY(1, 0, label.length() + 2, ' ', radioGroupColor);
-        getScreen().putStringXY(2, 0, label, radioGroupColor);
+        hLineXY(1, 0, label.length() + 2, ' ', radioGroupColor);
+        putStringXY(2, 0, label, radioGroupColor);
     }
 
     // ------------------------------------------------------------------------
index 85be9ed54b57bcfe944baa30c569fbb05274e851..7d15b2897ab860ba425ad73cc12b9d70db20c0c1 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index c5f122bb024977f9a2017cca62c903041dbf5b9f..6817f40e13a1fa90f927bc5243501eb298af0aa2 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index cdc5c0f4ed9b5bee9d9dda45e708e0cd471c56f8..881e1a7d8cddf08ee6fc448aae7a56d6acb93da0 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -162,10 +162,8 @@ public class TSpinner extends TWidget {
             spinnerColor = getTheme().getColor("tspinner.inactive");
         }
 
-        getScreen().putCharXY(getWidth() - 2, 0, GraphicsChars.UPARROW,
-            spinnerColor);
-        getScreen().putCharXY(getWidth() - 1, 0, GraphicsChars.DOWNARROW,
-            spinnerColor);
+        putCharXY(getWidth() - 2, 0, GraphicsChars.UPARROW, spinnerColor);
+        putCharXY(getWidth() - 1, 0, GraphicsChars.DOWNARROW, spinnerColor);
     }
 
     // ------------------------------------------------------------------------
index f3b8038d41518c1245dccd2a18685cc663e8ec79..72a0ec6662163740385f887ecd701fcf5ae4c73f 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -259,35 +259,34 @@ public class TStatusBar extends TWidget {
         int row = getScreen().getHeight() - 1;
         int width = getScreen().getWidth();
 
-        getScreen().hLineXY(0, row, width, ' ', barColor);
+        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);
+                putCharXY(col++, row, ' ', selectedColor);
+                putStringXY(col, row, keyStr, selectedColor);
                 col += keyStr.length();
-                getScreen().putCharXY(col++, row, ' ', selectedColor);
-                getScreen().putStringXY(col, row, key.label, selectedColor);
+                putCharXY(col++, row, ' ', selectedColor);
+                putStringXY(col, row, key.label, selectedColor);
                 col += key.label.length();
-                getScreen().putCharXY(col++, row, ' ', selectedColor);
+                putCharXY(col++, row, ' ', selectedColor);
             } else {
-                getScreen().putCharXY(col++, row, ' ', barColor);
-                getScreen().putStringXY(col, row, keyStr, keyColor);
+                putCharXY(col++, row, ' ', barColor);
+                putStringXY(col, row, keyStr, keyColor);
                 col += keyStr.length() + 1;
-                getScreen().putStringXY(col, row, key.label, barColor);
+                putStringXY(col, row, key.label, barColor);
                 col += key.label.length();
-                getScreen().putCharXY(col++, row, ' ', barColor);
+                putCharXY(col++, row, ' ', barColor);
             }
         }
         if (text.length() > 0) {
             if (keys.size() > 0) {
-                getScreen().putCharXY(col++, row, GraphicsChars.VERTICAL_BAR,
-                    barColor);
+                putCharXY(col++, row, GraphicsChars.VERTICAL_BAR, barColor);
             }
-            getScreen().putCharXY(col++, row, ' ', barColor);
-            getScreen().putStringXY(col, row, text, barColor);
+            putCharXY(col++, row, ' ', barColor);
+            putStringXY(col, row, text, barColor);
         }
     }
 
index 74f71edc7c0af8b9387dddca88a246d275810a9a..a624e6c50574709fe4acd969184c62a5d6dc76ae 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -31,7 +31,7 @@ package jexer;
 import java.io.IOException;
 import java.lang.reflect.Field;
 import java.text.MessageFormat;
-import java.util.LinkedList;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.ResourceBundle;
@@ -77,10 +77,15 @@ public class TTerminalWindow extends TScrollableWindow
     /**
      * If true, we are using the ptypipe utility to support dynamic window
      * resizing.  ptypipe is available at
-     * https://github.com/klamonte/ptypipe .
+     * https://gitlab.com/klamonte/ptypipe .
      */
     private boolean ptypipe = false;
 
+    /**
+     * If true, close the window when the shell exits.
+     */
+    private boolean closeOnExit = false;
+
     // ------------------------------------------------------------------------
     // Constructors -----------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -96,7 +101,25 @@ public class TTerminalWindow extends TScrollableWindow
     public TTerminalWindow(final TApplication application, final int x,
         final int y, final String commandLine) {
 
-        this(application, x, y, RESIZABLE, commandLine.split("\\s"));
+        this(application, x, y, RESIZABLE, commandLine.split("\\s"),
+            System.getProperty("jexer.TTerminal.closeOnExit",
+                "false").equals("true"));
+    }
+
+    /**
+     * Public constructor spawns a custom command line.
+     *
+     * @param application TApplication that manages this window
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param commandLine the command line to execute
+     * @param closeOnExit if true, close the window when the command exits
+     */
+    public TTerminalWindow(final TApplication application, final int x,
+        final int y, final String commandLine, final boolean closeOnExit) {
+
+        this(application, x, y, RESIZABLE, commandLine.split("\\s"),
+            closeOnExit);
     }
 
     /**
@@ -111,9 +134,30 @@ public class TTerminalWindow extends TScrollableWindow
     public TTerminalWindow(final TApplication application, final int x,
         final int y, final int flags, final String [] command) {
 
+        this(application, x, y, flags, command,
+            System.getProperty("jexer.TTerminal.closeOnExit",
+                "false").equals("true"));
+    }
+
+    /**
+     * Public constructor spawns a custom command line.
+     *
+     * @param application TApplication that manages this window
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param flags mask of CENTERED, MODAL, or RESIZABLE
+     * @param command the command line to execute
+     * @param closeOnExit if true, close the window when the command exits
+     */
+    public TTerminalWindow(final TApplication application, final int x,
+        final int y, final int flags, final String [] command,
+        final boolean closeOnExit) {
+
         super(application, i18n.getString("windowTitle"), x, y,
             80 + 2, 24 + 2, flags);
 
+        this.closeOnExit = closeOnExit;
+
         String [] fullCommand;
 
         // Spawn a shell and pass its I/O to the other constructor.
@@ -161,9 +205,29 @@ public class TTerminalWindow extends TScrollableWindow
     public TTerminalWindow(final TApplication application, final int x,
         final int y, final int flags) {
 
+        this(application, x, y, flags,
+            System.getProperty("jexer.TTerminal.closeOnExit",
+                "false").equals("true"));
+
+    }
+
+    /**
+     * Public constructor spawns a shell.
+     *
+     * @param application TApplication that manages this window
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param flags mask of CENTERED, MODAL, or RESIZABLE
+     * @param closeOnExit if true, close the window when the shell exits
+     */
+    public TTerminalWindow(final TApplication application, final int x,
+        final int y, final int flags, final boolean closeOnExit) {
+
         super(application, i18n.getString("windowTitle"), x, y,
             80 + 2, 24 + 2, flags);
 
+        this.closeOnExit = closeOnExit;
+
         String cmdShellWindows = "cmd.exe";
 
         // You cannot run a login shell in a bare Process interactively, due
@@ -224,7 +288,7 @@ public class TTerminalWindow extends TScrollableWindow
                 + getVerticalValue();
             assert (visibleBottom >= 0);
 
-            List<DisplayLine> preceedingBlankLines = new LinkedList<DisplayLine>();
+            List<DisplayLine> preceedingBlankLines = new ArrayList<DisplayLine>();
             int visibleTop = visibleBottom - visibleHeight;
             if (visibleTop < 0) {
                 for (int i = visibleTop; i < 0; i++) {
@@ -234,11 +298,11 @@ public class TTerminalWindow extends TScrollableWindow
             }
             assert (visibleTop >= 0);
 
-            List<DisplayLine> displayLines = new LinkedList<DisplayLine>();
+            List<DisplayLine> displayLines = new ArrayList<DisplayLine>();
             displayLines.addAll(scrollback);
             displayLines.addAll(display);
 
-            List<DisplayLine> visibleLines = new LinkedList<DisplayLine>();
+            List<DisplayLine> visibleLines = new ArrayList<DisplayLine>();
             visibleLines.addAll(preceedingBlankLines);
             visibleLines.addAll(displayLines.subList(visibleTop,
                     visibleBottom));
@@ -277,10 +341,10 @@ public class TTerminalWindow extends TScrollableWindow
                         }
                     }
                     if (line.isDoubleWidth()) {
-                        getScreen().putCharXY((i * 2) + 1, row, newCell);
-                        getScreen().putCharXY((i * 2) + 2, row, ' ', newCell);
+                        putCharXY((i * 2) + 1, row, newCell);
+                        putCharXY((i * 2) + 2, row, ' ', newCell);
                     } else {
-                        getScreen().putCharXY(i + 1, row, newCell);
+                        putCharXY(i + 1, row, newCell);
                     }
                 }
                 row++;
@@ -292,8 +356,7 @@ public class TTerminalWindow extends TScrollableWindow
             CellAttributes background = new CellAttributes();
             // Fill in the blank lines on bottom
             for (int i = 0; i < visibleHeight; i++) {
-                getScreen().hLineXY(1, i + row, getWidth() - 2, ' ',
-                    background);
+                hLineXY(1, i + row, getWidth() - 2, ' ', background);
             }
 
         } // synchronized (emulator)
@@ -711,6 +774,9 @@ public class TTerminalWindow extends TScrollableWindow
      * Hook for subclasses to be notified of the shell termination.
      */
     public void onShellExit() {
+        if (closeOnExit) {
+            close();
+        }
         getApplication().postEvent(new TMenuEvent(TMenu.MID_REPAINT));
     }
 
index 44176a4efa8979143b77cb74788c308824ade5c1..60f0e585c9d4faede83c681011a97d8a27a4bbf0 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -28,6 +28,7 @@
  */
 package jexer;
 
+import java.util.Arrays;
 import java.util.LinkedList;
 import java.util.List;
 
@@ -57,6 +58,12 @@ public class TText extends TScrollableWidget {
      * Available text justifications.
      */
     public enum Justification {
+
+        /**
+         * Not justified at all, use spacing as provided by the client.
+         */
+        NONE,
+
         /**
          * Left-justified text.
          */
@@ -183,8 +190,7 @@ public class TText extends TScrollableWidget {
                 line = "";
             }
             String formatString = "%-" + Integer.toString(getWidth() - 1) + "s";
-            getScreen().putStringXY(0, topY, String.format(formatString, line),
-                    color);
+            putStringXY(0, topY, String.format(formatString, line), color);
             topY++;
 
             if (topY >= (getHeight() - 1)) {
@@ -194,7 +200,7 @@ public class TText extends TScrollableWidget {
 
         // Pad the rest with blank lines
         for (int i = topY; i < (getHeight() - 1); i++) {
-            getScreen().hLineXY(0, i, getWidth() - 1, ' ', color);
+            hLineXY(0, i, getWidth() - 1, ' ', color);
         }
 
     }
@@ -260,6 +266,9 @@ public class TText extends TScrollableWidget {
         String[] paragraphs = text.split("\n\n");
         for (String p : paragraphs) {
             switch (justification) {
+            case NONE:
+                lines.addAll(Arrays.asList(p.split("\n")));
+                break;
             case LEFT:
                 lines.addAll(jexer.bits.StringUtils.left(p,
                         getWidth() - 1));
index 3ec9dbdb9a88c0d429c09a02a6e4ef8a7bffea4e..8007153d679543d17b74fc51cb14663c262ac866 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index 9b99cfcaa035db05a38c55096be892387ffe3baf..444e058542c8d566be5395f2f51ee1e88ff57a28 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -212,21 +212,16 @@ public class TVScroller extends TWidget {
     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);
+        putCharXY(0, 0, GraphicsChars.CP437[0x1E], arrowColor);
+        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);
+            vLineXY(0, 1, getHeight() - 2, GraphicsChars.CP437[0xB1], barColor);
+            putCharXY(0, boxPosition(), GraphicsChars.BOX, arrowColor);
         } else {
-            getScreen().vLineXY(0, 1, getHeight() - 2, GraphicsChars.HATCH,
-                barColor);
+            vLineXY(0, 1, getHeight() - 2, GraphicsChars.HATCH, barColor);
         }
-
     }
 
     // ------------------------------------------------------------------------
index 6d5d147b3174ea39ab80da901dbc362a47c442b3..4a4ba2ce18190f361ba1830d4983b66a62565ec3 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -33,6 +33,8 @@ import java.util.List;
 import java.util.ArrayList;
 
 import jexer.backend.Screen;
+import jexer.bits.Cell;
+import jexer.bits.CellAttributes;
 import jexer.bits.ColorTheme;
 import jexer.event.TCommandEvent;
 import jexer.event.TInputEvent;
@@ -180,8 +182,9 @@ public abstract class TWidget implements Comparable<TWidget> {
         this.window = parent.window;
         children = new ArrayList<TWidget>();
 
-        // Do not add TStatusBars, they are drawn by TApplication
+        // Do not add TStatusBars, they are drawn by TApplication.
         if (this instanceof TStatusBar) {
+            // NOP
         } else {
             parent.addChild(this);
         }
@@ -205,8 +208,9 @@ public abstract class TWidget implements Comparable<TWidget> {
         this.window = parent.window;
         children = new ArrayList<TWidget>();
 
-        // Do not add TStatusBars, they are drawn by TApplication
+        // Do not add TStatusBars, they are drawn by TApplication.
         if (this instanceof TStatusBar) {
+            // NOP
         } else {
             parent.addChild(this);
         }
@@ -241,6 +245,17 @@ public abstract class TWidget implements Comparable<TWidget> {
     // Event handlers ---------------------------------------------------------
     // ------------------------------------------------------------------------
 
+    /**
+     * Subclasses should override this method to cleanup resources.  This is
+     * called by TWindow.onClose().
+     */
+    protected void close() {
+        // Default: call close() on children.
+        for (TWidget w: getChildren()) {
+            w.close();
+        }
+    }
+
     /**
      * Check if a mouse press/release event coordinate is contained in this
      * widget.
@@ -280,19 +295,20 @@ public abstract class TWidget implements Comparable<TWidget> {
         if ((children.size() == 0)
             || (this instanceof TTreeView)
             || (this instanceof TText)
+            || (this instanceof TComboBox)
         ) {
 
             // Defaults:
             //   tab / shift-tab - switch to next/previous widget
             //   left-arrow or up-arrow: same as shift-tab
             if ((keypress.equals(kbTab))
-                || (keypress.equals(kbDown))
+                || (keypress.equals(kbDown) && !(this instanceof TComboBox))
             ) {
                 parent.switchWidget(true);
                 return;
             } else if ((keypress.equals(kbShiftTab))
                 || (keypress.equals(kbBackTab))
-                || (keypress.equals(kbUp))
+                || (keypress.equals(kbUp) && !(this instanceof TComboBox))
             ) {
                 parent.switchWidget(false);
                 return;
@@ -349,6 +365,17 @@ public abstract class TWidget implements Comparable<TWidget> {
      */
     public void onMouseDown(final TMouseEvent mouse) {
         // Default: do nothing, pass to children instead
+        if (activeChild != null) {
+            if (activeChild.mouseWouldHit(mouse)) {
+                // Dispatch to the active child
+
+                // Set x and y relative to the child's coordinates
+                mouse.setX(mouse.getAbsoluteX() - activeChild.getAbsoluteX());
+                mouse.setY(mouse.getAbsoluteY() - activeChild.getAbsoluteY());
+                activeChild.onMouseDown(mouse);
+                return;
+            }
+        }
         for (int i = children.size() - 1 ; i >= 0 ; i--) {
             TWidget widget = children.get(i);
             if (widget.mouseWouldHit(mouse)) {
@@ -371,6 +398,17 @@ public abstract class TWidget implements Comparable<TWidget> {
      */
     public void onMouseUp(final TMouseEvent mouse) {
         // Default: do nothing, pass to children instead
+        if (activeChild != null) {
+            if (activeChild.mouseWouldHit(mouse)) {
+                // Dispatch to the active child
+
+                // Set x and y relative to the child's coordinates
+                mouse.setX(mouse.getAbsoluteX() - activeChild.getAbsoluteX());
+                mouse.setY(mouse.getAbsoluteY() - activeChild.getAbsoluteY());
+                activeChild.onMouseUp(mouse);
+                return;
+            }
+        }
         for (int i = children.size() - 1 ; i >= 0 ; i--) {
             TWidget widget = children.get(i);
             if (widget.mouseWouldHit(mouse)) {
@@ -410,6 +448,17 @@ public abstract class TWidget implements Comparable<TWidget> {
      */
     public void onMouseDoubleClick(final TMouseEvent mouse) {
         // Default: do nothing, pass to children instead
+        if (activeChild != null) {
+            if (activeChild.mouseWouldHit(mouse)) {
+                // Dispatch to the active child
+
+                // Set x and y relative to the child's coordinates
+                mouse.setX(mouse.getAbsoluteX() - activeChild.getAbsoluteX());
+                mouse.setY(mouse.getAbsoluteY() - activeChild.getAbsoluteY());
+                activeChild.onMouseDoubleClick(mouse);
+                return;
+            }
+        }
         for (int i = children.size() - 1 ; i >= 0 ; i--) {
             TWidget widget = children.get(i);
             if (widget.mouseWouldHit(mouse)) {
@@ -931,7 +980,7 @@ public abstract class TWidget implements Comparable<TWidget> {
      *
      * @return the ColorTheme
      */
-    public final ColorTheme getTheme() {
+    protected final ColorTheme getTheme() {
         return window.getApplication().getTheme();
     }
 
@@ -944,9 +993,9 @@ public abstract class TWidget implements Comparable<TWidget> {
     }
 
     /**
-     * Called by parent to render to TWindow.
+     * Called by parent to render to TWindow.  Note package private access.
      */
-    public final void drawChildren() {
+    final void drawChildren() {
         // Set my clipping rectangle
         assert (window != null);
         assert (getScreen() != null);
@@ -993,18 +1042,22 @@ public abstract class TWidget implements Comparable<TWidget> {
         // Draw me
         draw();
 
-        // Continue down the chain
+        // Continue down the chain.  Draw the active child last so that it
+        // is on top.
         for (TWidget widget: children) {
-            if (widget.isVisible()) {
+            if (widget.isVisible() && (widget != activeChild)) {
                 widget.drawChildren();
             }
         }
+        if (activeChild != null) {
+            activeChild.drawChildren();
+        }
     }
 
     /**
      * Repaint the screen on the next update.
      */
-    public final void doRepaint() {
+    protected final void doRepaint() {
         window.getApplication().doRepaint();
     }
 
@@ -1157,6 +1210,204 @@ public abstract class TWidget implements Comparable<TWidget> {
         return this;
     }
 
+    // ------------------------------------------------------------------------
+    // Passthru for Screen functions ------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Get the attributes at one location.
+     *
+     * @param x column coordinate.  0 is the left-most column.
+     * @param y row coordinate.  0 is the top-most row.
+     * @return attributes at (x, y)
+     */
+    protected final CellAttributes getAttrXY(final int x, final int y) {
+        return getScreen().getAttrXY(x, y);
+    }
+
+    /**
+     * Set the attributes at one location.
+     *
+     * @param x column coordinate.  0 is the left-most column.
+     * @param y row coordinate.  0 is the top-most row.
+     * @param attr attributes to use (bold, foreColor, backColor)
+     */
+    protected final void putAttrXY(final int x, final int y,
+        final CellAttributes attr) {
+
+        getScreen().putAttrXY(x, y, attr);
+    }
+
+    /**
+     * Set the attributes at one location.
+     *
+     * @param x column coordinate.  0 is the left-most column.
+     * @param y row coordinate.  0 is the top-most row.
+     * @param attr attributes to use (bold, foreColor, backColor)
+     * @param clip if true, honor clipping/offset
+     */
+    protected final void putAttrXY(final int x, final int y,
+        final CellAttributes attr, final boolean clip) {
+
+        getScreen().putAttrXY(x, y, attr, clip);
+    }
+
+    /**
+     * Fill the entire screen with one character with attributes.
+     *
+     * @param ch character to draw
+     * @param attr attributes to use (bold, foreColor, backColor)
+     */
+    protected final void putAll(final char ch, final CellAttributes attr) {
+        getScreen().putAll(ch, attr);
+    }
+
+    /**
+     * Render one character with attributes.
+     *
+     * @param x column coordinate.  0 is the left-most column.
+     * @param y row coordinate.  0 is the top-most row.
+     * @param ch character + attributes to draw
+     */
+    protected final void putCharXY(final int x, final int y, final Cell ch) {
+        getScreen().putCharXY(x, y, ch);
+    }
+
+    /**
+     * Render one character with attributes.
+     *
+     * @param x column coordinate.  0 is the left-most column.
+     * @param y row coordinate.  0 is the top-most row.
+     * @param ch character to draw
+     * @param attr attributes to use (bold, foreColor, backColor)
+     */
+    protected final void putCharXY(final int x, final int y, final char ch,
+        final CellAttributes attr) {
+
+        getScreen().putCharXY(x, y, ch, attr);
+    }
+
+    /**
+     * Render one character without changing the underlying attributes.
+     *
+     * @param x column coordinate.  0 is the left-most column.
+     * @param y row coordinate.  0 is the top-most row.
+     * @param ch character to draw
+     */
+    protected final void putCharXY(final int x, final int y, final char ch) {
+        getScreen().putCharXY(x, y, ch);
+    }
+
+    /**
+     * Render a string.  Does not wrap if the string exceeds the line.
+     *
+     * @param x column coordinate.  0 is the left-most column.
+     * @param y row coordinate.  0 is the top-most row.
+     * @param str string to draw
+     * @param attr attributes to use (bold, foreColor, backColor)
+     */
+    protected final void putStringXY(final int x, final int y, final String str,
+        final CellAttributes attr) {
+
+        getScreen().putStringXY(x, y, str, attr);
+    }
+
+    /**
+     * Render a string without changing the underlying attribute.  Does not
+     * wrap if the string exceeds the line.
+     *
+     * @param x column coordinate.  0 is the left-most column.
+     * @param y row coordinate.  0 is the top-most row.
+     * @param str string to draw
+     */
+    protected final void putStringXY(final int x, final int y, final String str) {
+        getScreen().putStringXY(x, y, str);
+    }
+
+    /**
+     * Draw a vertical line from (x, y) to (x, y + n).
+     *
+     * @param x column coordinate.  0 is the left-most column.
+     * @param y row coordinate.  0 is the top-most row.
+     * @param n number of characters to draw
+     * @param ch character to draw
+     * @param attr attributes to use (bold, foreColor, backColor)
+     */
+    protected final void vLineXY(final int x, final int y, final int n,
+        final char ch, final CellAttributes attr) {
+
+        getScreen().vLineXY(x, y, n, ch, attr);
+    }
+
+    /**
+     * Draw a horizontal line from (x, y) to (x + n, y).
+     *
+     * @param x column coordinate.  0 is the left-most column.
+     * @param y row coordinate.  0 is the top-most row.
+     * @param n number of characters to draw
+     * @param ch character to draw
+     * @param attr attributes to use (bold, foreColor, backColor)
+     */
+    protected final void hLineXY(final int x, final int y, final int n,
+        final char ch, final CellAttributes attr) {
+
+        getScreen().hLineXY(x, y, n, ch, attr);
+    }
+
+    /**
+     * Draw a box with a border and empty background.
+     *
+     * @param left left column of box.  0 is the left-most row.
+     * @param top top row of the box.  0 is the top-most row.
+     * @param right right column of box
+     * @param bottom bottom row of the box
+     * @param border attributes to use for the border
+     * @param background attributes to use for the background
+     */
+    protected final void drawBox(final int left, final int top,
+        final int right, final int bottom,
+        final CellAttributes border, final CellAttributes background) {
+
+        getScreen().drawBox(left, top, right, bottom, border, background);
+    }
+
+    /**
+     * Draw a box with a border and empty background.
+     *
+     * @param left left column of box.  0 is the left-most row.
+     * @param top top row of the box.  0 is the top-most row.
+     * @param right right column of box
+     * @param bottom bottom row of the box
+     * @param border attributes to use for the border
+     * @param background attributes to use for the background
+     * @param borderType if 1, draw a single-line border; if 2, draw a
+     * double-line border; if 3, draw double-line top/bottom edges and
+     * single-line left/right edges (like Qmodem)
+     * @param shadow if true, draw a "shadow" on the box
+     */
+    protected final void drawBox(final int left, final int top,
+        final int right, final int bottom,
+        final CellAttributes border, final CellAttributes background,
+        final int borderType, final boolean shadow) {
+
+        getScreen().drawBox(left, top, right, bottom, border, background,
+            borderType, shadow);
+    }
+
+    /**
+     * Draw a box shadow.
+     *
+     * @param left left column of box.  0 is the left-most row.
+     * @param top top row of the box.  0 is the top-most row.
+     * @param right right column of box
+     * @param bottom bottom row of the box
+     */
+    protected final void drawBoxShadow(final int left, final int top,
+        final int right, final int bottom) {
+
+        getScreen().drawBoxShadow(left, top, right, bottom);
+    }
+
     // ------------------------------------------------------------------------
     // Other TWidget constructors ---------------------------------------------
     // ------------------------------------------------------------------------
@@ -1579,6 +1830,17 @@ public abstract class TWidget implements Comparable<TWidget> {
         return getApplication().fileOpenBox(path);
     }
 
+    /**
+     * Convenience function to spawn a file save box.
+     *
+     * @param path path of selected file
+     * @return the result of the new file open box
+     * @throws IOException if a java.io operation throws
+     */
+    public final String fileSaveBox(final String path) throws IOException {
+        return getApplication().fileOpenBox(path, TFileOpenBox.Type.SAVE);
+    }
+
     /**
      * Convenience function to spawn a file open box.
      *
@@ -1592,6 +1854,41 @@ public abstract class TWidget implements Comparable<TWidget> {
 
         return getApplication().fileOpenBox(path, type);
     }
+
+    /**
+     * Convenience function to spawn a file open box.
+     *
+     * @param path path of selected file
+     * @param type one of the Type constants
+     * @param filter a string that files must match to be displayed
+     * @return the result of the new file open box
+     * @throws IOException of a java.io operation throws
+     */
+    public final String fileOpenBox(final String path,
+        final TFileOpenBox.Type type, final String filter) throws IOException {
+
+        ArrayList<String> filters = new ArrayList<String>();
+        filters.add(filter);
+
+        return getApplication().fileOpenBox(path, type, filters);
+    }
+
+    /**
+     * Convenience function to spawn a file open box.
+     *
+     * @param path path of selected file
+     * @param type one of the Type constants
+     * @param filters a list of strings that files must match to be displayed
+     * @return the result of the new file open box
+     * @throws IOException of a java.io operation throws
+     */
+    public final String fileOpenBox(final String path,
+        final TFileOpenBox.Type type,
+        final List<String> filters) throws IOException {
+
+        return getApplication().fileOpenBox(path, type, filters);
+    }
+
     /**
      * Convenience function to add a directory list to this container/window.
      *
@@ -1616,7 +1913,8 @@ public abstract class TWidget implements Comparable<TWidget> {
      * @param y row relative to parent
      * @param width width of text area
      * @param height height of text area
-     * @param action action to perform when an item is selected
+     * @param action action to perform when an item is selected (enter or
+     * double-click)
      * @return the new directory list
      */
     public final TDirectoryList addDirectoryList(final String path, final int x,
@@ -1628,6 +1926,51 @@ public abstract class TWidget implements Comparable<TWidget> {
     /**
      * Convenience function to add a directory list to this container/window.
      *
+     * @param path directory path, must be a directory
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param width width of text area
+     * @param height height of text area
+     * @param action action to perform when an item is selected (enter or
+     * double-click)
+     * @param singleClickAction action to perform when an item is selected
+     * (single-click)
+     * @return the new directory list
+     */
+    public final TDirectoryList addDirectoryList(final String path, final int x,
+        final int y, final int width, final int height, final TAction action,
+        final TAction singleClickAction) {
+
+        return new TDirectoryList(this, path, x, y, width, height, action,
+            singleClickAction);
+    }
+
+    /**
+     * Convenience function to add a directory list to this container/window.
+     *
+     * @param path directory path, must be a directory
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param width width of text area
+     * @param height height of text area
+     * @param action action to perform when an item is selected (enter or
+     * double-click)
+     * @param singleClickAction action to perform when an item is selected
+     * (single-click)
+     * @param filters a list of strings that files must match to be displayed
+     * @return the new directory list
+     */
+    public final TDirectoryList addDirectoryList(final String path, final int x,
+        final int y, final int width, final int height, final TAction action,
+        final TAction singleClickAction, final List<String> filters) {
+
+        return new TDirectoryList(this, path, x, y, width, height, action,
+            singleClickAction, filters);
+    }
+
+    /**
+     * Convenience function to add a list to this container/window.
+     *
      * @param strings list of strings to show
      * @param x column relative to parent
      * @param y row relative to parent
@@ -1642,7 +1985,7 @@ public abstract class TWidget implements Comparable<TWidget> {
     }
 
     /**
-     * Convenience function to add a directory list to this container/window.
+     * Convenience function to add a list to this container/window.
      *
      * @param strings list of strings to show
      * @param x column relative to parent
@@ -1660,7 +2003,7 @@ public abstract class TWidget implements Comparable<TWidget> {
     }
 
     /**
-     * Convenience function to add a directory list to this container/window.
+     * Convenience function to add a list to this container/window.
      *
      * @param strings list of strings to show
      * @param x column relative to parent
index 140a38aa263f1c2138aed7ff558ada47df9507e1..a4a9c23a815d602e2525b07f28bfbfcd921744a7 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -337,19 +337,32 @@ public class TWindow extends TWidget {
         return false;
     }
 
+    /**
+     * Subclasses should override this method to perform any user prompting
+     * before they are offscreen.  Note that unlike other windowing toolkits,
+     * windows can NOT use this function in some manner to avoid being
+     * closed.  This is called by application.closeWindow().
+     */
+    protected void onPreClose() {
+        // Default: do nothing.
+    }
+
     /**
      * Subclasses should override this method to cleanup resources.  This is
      * called by application.closeWindow().
      */
-    public void onClose() {
-        // Default: do nothing
+    protected void onClose() {
+        // Default: perform widget-specific cleanup.
+        for (TWidget w: getChildren()) {
+            w.close();
+        }
     }
 
     /**
      * Called by application.switchWindow() when this window gets the
      * focus, and also by application.addWindow().
      */
-    public void onFocus() {
+    protected void onFocus() {
         // Default: do nothing
     }
 
@@ -357,21 +370,21 @@ public class TWindow extends TWidget {
      * Called by application.switchWindow() when another window gets the
      * focus.
      */
-    public void onUnfocus() {
+    protected void onUnfocus() {
         // Default: do nothing
     }
 
     /**
      * Called by application.hideWindow().
      */
-    public void onHide() {
+    protected void onHide() {
         // Default: do nothing
     }
 
     /**
      * Called by application.showWindow().
      */
-    public void onShow() {
+    protected void onShow() {
         // Default: do nothing
     }
 
@@ -385,6 +398,8 @@ public class TWindow extends TWidget {
         this.mouse = mouse;
 
         inKeyboardResize = false;
+        inWindowMove = false;
+        inWindowResize = false;
 
         if ((mouse.getAbsoluteY() == getY())
             && mouse.isMouse1()
@@ -854,8 +869,8 @@ public class TWindow extends TWidget {
         CellAttributes background = getBackground();
         int borderType = getBorderType();
 
-        getScreen().drawBox(0, 0, getWidth(), getHeight(), border,
-            background, borderType, true);
+        drawBox(0, 0, getWidth(), getHeight(), border, background, borderType,
+            true);
 
         // Draw the title
         int titleLeft = (getWidth() - title.length() - 2) / 2;
@@ -1264,7 +1279,15 @@ public class TWindow extends TWidget {
         if (!isModal()
             && (inWindowMove || inWindowResize || inKeyboardResize)
         ) {
-            assert (isActive());
+            if (!isActive()) {
+                // The user's terminal never passed a mouse up event, and now
+                // another window is active but we never finished a drag.
+                inWindowMove = false;
+                inWindowResize = false;
+                inKeyboardResize = false;
+                return getTheme().getColor("twindow.border.inactive");
+            }
+
             return getTheme().getColor("twindow.border.windowmove");
         } else if (isModal() && inWindowMove) {
             assert (isActive());
@@ -1323,148 +1346,4 @@ public class TWindow extends TWidget {
         }
     }
 
-    // ------------------------------------------------------------------------
-    // Passthru for Screen functions ------------------------------------------
-    // ------------------------------------------------------------------------
-
-    /**
-     * Get the attributes at one location.
-     *
-     * @param x column coordinate.  0 is the left-most column.
-     * @param y row coordinate.  0 is the top-most row.
-     * @return attributes at (x, y)
-     */
-    public final CellAttributes getAttrXY(final int x, final int y) {
-        return getScreen().getAttrXY(x, y);
-    }
-
-    /**
-     * Set the attributes at one location.
-     *
-     * @param x column coordinate.  0 is the left-most column.
-     * @param y row coordinate.  0 is the top-most row.
-     * @param attr attributes to use (bold, foreColor, backColor)
-     */
-    public final void putAttrXY(final int x, final int y,
-        final CellAttributes attr) {
-
-        getScreen().putAttrXY(x, y, attr);
-    }
-
-    /**
-     * Set the attributes at one location.
-     *
-     * @param x column coordinate.  0 is the left-most column.
-     * @param y row coordinate.  0 is the top-most row.
-     * @param attr attributes to use (bold, foreColor, backColor)
-     * @param clip if true, honor clipping/offset
-     */
-    public final void putAttrXY(final int x, final int y,
-        final CellAttributes attr, final boolean clip) {
-
-        getScreen().putAttrXY(x, y, attr, clip);
-    }
-
-    /**
-     * Fill the entire screen with one character with attributes.
-     *
-     * @param ch character to draw
-     * @param attr attributes to use (bold, foreColor, backColor)
-     */
-    public final void putAll(final char ch, final CellAttributes attr) {
-        getScreen().putAll(ch, attr);
-    }
-
-    /**
-     * Render one character with attributes.
-     *
-     * @param x column coordinate.  0 is the left-most column.
-     * @param y row coordinate.  0 is the top-most row.
-     * @param ch character + attributes to draw
-     */
-    public final void putCharXY(final int x, final int y, final Cell ch) {
-        getScreen().putCharXY(x, y, ch);
-    }
-
-    /**
-     * Render one character with attributes.
-     *
-     * @param x column coordinate.  0 is the left-most column.
-     * @param y row coordinate.  0 is the top-most row.
-     * @param ch character to draw
-     * @param attr attributes to use (bold, foreColor, backColor)
-     */
-    public final void putCharXY(final int x, final int y, final char ch,
-        final CellAttributes attr) {
-
-        getScreen().putCharXY(x, y, ch, attr);
-    }
-
-    /**
-     * Render one character without changing the underlying attributes.
-     *
-     * @param x column coordinate.  0 is the left-most column.
-     * @param y row coordinate.  0 is the top-most row.
-     * @param ch character to draw
-     */
-    public final void putCharXY(final int x, final int y, final char ch) {
-        getScreen().putCharXY(x, y, ch);
-    }
-
-    /**
-     * Render a string.  Does not wrap if the string exceeds the line.
-     *
-     * @param x column coordinate.  0 is the left-most column.
-     * @param y row coordinate.  0 is the top-most row.
-     * @param str string to draw
-     * @param attr attributes to use (bold, foreColor, backColor)
-     */
-    public final void putStringXY(final int x, final int y, final String str,
-        final CellAttributes attr) {
-
-        getScreen().putStringXY(x, y, str, attr);
-    }
-
-    /**
-     * Render a string without changing the underlying attribute.  Does not
-     * wrap if the string exceeds the line.
-     *
-     * @param x column coordinate.  0 is the left-most column.
-     * @param y row coordinate.  0 is the top-most row.
-     * @param str string to draw
-     */
-    public final void putStringXY(final int x, final int y, final String str) {
-        getScreen().putStringXY(x, y, str);
-    }
-
-    /**
-     * Draw a vertical line from (x, y) to (x, y + n).
-     *
-     * @param x column coordinate.  0 is the left-most column.
-     * @param y row coordinate.  0 is the top-most row.
-     * @param n number of characters to draw
-     * @param ch character to draw
-     * @param attr attributes to use (bold, foreColor, backColor)
-     */
-    public final void vLineXY(final int x, final int y, final int n,
-        final char ch, final CellAttributes attr) {
-
-        getScreen().vLineXY(x, y, n, ch, attr);
-    }
-
-    /**
-     * Draw a horizontal line from (x, y) to (x + n, y).
-     *
-     * @param x column coordinate.  0 is the left-most column.
-     * @param y row coordinate.  0 is the top-most row.
-     * @param n number of characters to draw
-     * @param ch character to draw
-     * @param attr attributes to use (bold, foreColor, backColor)
-     */
-    public final void hLineXY(final int x, final int y, final int n,
-        final char ch, final CellAttributes attr) {
-
-        getScreen().hLineXY(x, y, n, ch, attr);
-    }
-
 }
index 20efa7eeaec8ad913387a4a546dcab512f41d512..8bd1816cd55053b461c0187896db966ac5aa492c 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index 7aad3a5acbebed52b70a272a24ee79c41333341b..0614e17d9ccc6bb5ba690da0bbd30d39ec3f5851 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index 84f6528c5a348832642bc21315dea8b1ddbb5bbc..1dc3957d75b534deec2b318a9c2bd501db8d7f5f 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -28,6 +28,7 @@
  */
 package jexer.backend;
 
+import java.awt.image.BufferedImage;
 import java.io.BufferedReader;
 import java.io.FileDescriptor;
 import java.io.FileInputStream;
@@ -40,9 +41,13 @@ import java.io.PrintWriter;
 import java.io.Reader;
 import java.io.UnsupportedEncodingException;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 import java.util.LinkedList;
+import java.util.Map;
 
+import jexer.TImage;
 import jexer.bits.Cell;
 import jexer.bits.CellAttributes;
 import jexer.bits.Color;
@@ -76,6 +81,12 @@ public class ECMA48Terminal extends LogicalScreen
         MOUSE_SGR,
     }
 
+    /**
+     * Number of colors in the sixel palette.  Xterm 335 defines the max as
+     * 1024.
+     */
+    private static final int MAX_COLOR_REGISTERS = 1024;
+
     // ------------------------------------------------------------------------
     // Variables --------------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -162,6 +173,31 @@ public class ECMA48Terminal extends LogicalScreen
      */
     private TResizeEvent windowResize = null;
 
+    /**
+     * Window width in pixels.  Used for sixel support.
+     */
+    private int widthPixels = 640;
+
+    /**
+     * Window height in pixels.  Used for sixel support.
+     */
+    private int heightPixels = 400;
+
+    /**
+     * If true, emit image data via sixel.
+     */
+    private boolean sixel = true;
+
+    /**
+     * The sixel palette handler.
+     */
+    private SixelPalette palette = null;
+
+    /**
+     * The sixel post-rendered string cache.
+     */
+    private SixelCache sixelCache = null;
+
     /**
      * If true, then we changed System.in and need to change it back.
      */
@@ -194,6 +230,753 @@ public class ECMA48Terminal extends LogicalScreen
      */
     private Object listener;
 
+    /**
+     * SixelPalette is used to manage the conversion of images between 24-bit
+     * RGB color and a palette of MAX_COLOR_REGISTERS colors.
+     */
+    private class SixelPalette {
+
+        /**
+         * Color palette for sixel output, sorted low to high.
+         */
+        private List<Integer> rgbColors = new ArrayList<Integer>();
+
+        /**
+         * Map of color palette index for sixel output, from the order it was
+         * generated by makePalette() to rgbColors.
+         */
+        private int [] rgbSortedIndex = new int[MAX_COLOR_REGISTERS];
+
+        /**
+         * The color palette, organized by hue, saturation, and luminance.
+         * This is used for a fast color match.
+         */
+        private ArrayList<ArrayList<ArrayList<ColorIdx>>> hslColors;
+
+        /**
+         * Number of bits for hue.
+         */
+        private int hueBits = -1;
+
+        /**
+         * Number of bits for saturation.
+         */
+        private int satBits = -1;
+
+        /**
+         * Number of bits for luminance.
+         */
+        private int lumBits = -1;
+
+        /**
+         * Step size for hue bins.
+         */
+        private int hueStep = -1;
+
+        /**
+         * Step size for saturation bins.
+         */
+        private int satStep = -1;
+
+        /**
+         * Cached RGB to HSL result.
+         */
+        private int hsl[] = new int[3];
+
+        /**
+         * ColorIdx records a RGB color and its palette index.
+         */
+        private class ColorIdx {
+            /**
+             * The 24-bit RGB color.
+             */
+            public int color;
+
+            /**
+             * The palette index for this color.
+             */
+            public int index;
+
+            /**
+             * Public constructor.
+             *
+             * @param color the 24-bit RGB color
+             * @param index the palette index for this color
+             */
+            public ColorIdx(final int color, final int index) {
+                this.color = color;
+                this.index = index;
+            }
+        }
+
+        /**
+         * Public constructor.
+         */
+        public SixelPalette() {
+            makePalette();
+        }
+
+        /**
+         * Find the nearest match for a color in the palette.
+         *
+         * @param color the RGB color
+         * @return the index in rgbColors that is closest to color
+         */
+        public int matchColor(final int color) {
+
+            assert (color >= 0);
+
+            /*
+             * matchColor() is a critical performance bottleneck.  To make it
+             * decent, we do the following:
+             *
+             *   1. Find the nearest two hues that bracket this color.
+             *
+             *   2. Find the nearest two saturations that bracket this color.
+             *
+             *   3. Iterate within these four bands of luminance values,
+             *      returning the closest color by Euclidean distance.
+             *
+             * This strategy reduces the search space by about 97%.
+             */
+            int red   = (color >>> 16) & 0xFF;
+            int green = (color >>>  8) & 0xFF;
+            int blue  =  color         & 0xFF;
+
+            rgbToHsl(red, green, blue, hsl);
+            int hue = hsl[0];
+            int sat = hsl[1];
+            int lum = hsl[2];
+            // System.err.printf("%d %d %d\n", hue, sat, lum);
+
+            double diff = Double.MAX_VALUE;
+            int idx = -1;
+
+            int hue1 = hue / (360/hueStep);
+            int hue2 = hue1 + 1;
+            if (hue1 >= hslColors.size() - 1) {
+                // Bracket pure red from above.
+                hue1 = hslColors.size() - 1;
+                hue2 = 0;
+            } else if (hue1 == 0) {
+                // Bracket pure red from below.
+                hue2 = hslColors.size() - 1;
+            }
+
+            for (int hI = hue1; hI != -1;) {
+                ArrayList<ArrayList<ColorIdx>> sats = hslColors.get(hI);
+                if (hI == hue1) {
+                    hI = hue2;
+                } else if (hI == hue2) {
+                    hI = -1;
+                }
+
+                int sMin = (sat / satStep) - 1;
+                int sMax = sMin + 1;
+                if (sMin < 0) {
+                    sMin = 0;
+                    sMax = 1;
+                } else if (sMin == sats.size() - 1) {
+                    sMax = sMin;
+                    sMin--;
+                }
+                assert (sMin >= 0);
+                assert (sMax - sMin == 1);
+
+                // int sMin = 0;
+                // int sMax = sats.size() - 1;
+
+                for (int sI = sMin; sI <= sMax; sI++) {
+                    ArrayList<ColorIdx> lums = sats.get(sI);
+
+                    // True 3D colorspace match for the remaining values
+                    for (ColorIdx c: lums) {
+                        int rgbColor = c.color;
+                        double newDiff = 0;
+                        int red2   = (rgbColor >>> 16) & 0xFF;
+                        int green2 = (rgbColor >>>  8) & 0xFF;
+                        int blue2  =  rgbColor         & 0xFF;
+                        newDiff += Math.pow(red2 - red, 2);
+                        newDiff += Math.pow(green2 - green, 2);
+                        newDiff += Math.pow(blue2 - blue, 2);
+                        if (newDiff < diff) {
+                            idx = rgbSortedIndex[c.index];
+                            diff = newDiff;
+                        }
+                    }
+                }
+            }
+
+            if (((red * red) + (green * green) + (blue * blue)) < diff) {
+                // Black is a closer match.
+                idx = 0;
+            } else if ((((255 - red) * (255 - red)) +
+                    ((255 - green) * (255 - green)) +
+                    ((255 - blue) * (255 - blue))) < diff) {
+
+                // White is a closer match.
+                idx = MAX_COLOR_REGISTERS - 1;
+            }
+            assert (idx != -1);
+            return idx;
+        }
+
+        /**
+         * Clamp an int value to [0, 255].
+         *
+         * @param x the int value
+         * @return an int between 0 and 255.
+         */
+        private int clamp(final int x) {
+            if (x < 0) {
+                return 0;
+            }
+            if (x > 255) {
+                return 255;
+            }
+            return x;
+        }
+
+        /**
+         * Dither an image to a MAX_COLOR_REGISTERS palette.  The dithered
+         * image cells will contain indexes into the palette.
+         *
+         * @param image the image to dither
+         * @return the dithered image.  Every pixel is an index into the
+         * palette.
+         */
+        public BufferedImage ditherImage(final BufferedImage image) {
+
+            BufferedImage ditheredImage = new BufferedImage(image.getWidth(),
+                image.getHeight(), BufferedImage.TYPE_INT_ARGB);
+
+            int [] rgbArray = image.getRGB(0, 0, image.getWidth(),
+                image.getHeight(), null, 0, image.getWidth());
+            ditheredImage.setRGB(0, 0, image.getWidth(), image.getHeight(),
+                rgbArray, 0, image.getWidth());
+
+            for (int imageY = 0; imageY < image.getHeight(); imageY++) {
+                for (int imageX = 0; imageX < image.getWidth(); imageX++) {
+                    int oldPixel = ditheredImage.getRGB(imageX,
+                        imageY) & 0xFFFFFF;
+                    int colorIdx = matchColor(oldPixel);
+                    assert (colorIdx >= 0);
+                    assert (colorIdx < MAX_COLOR_REGISTERS);
+                    int newPixel = rgbColors.get(colorIdx);
+                    ditheredImage.setRGB(imageX, imageY, colorIdx);
+
+                    int oldRed   = (oldPixel >>> 16) & 0xFF;
+                    int oldGreen = (oldPixel >>>  8) & 0xFF;
+                    int oldBlue  =  oldPixel         & 0xFF;
+
+                    int newRed   = (newPixel >>> 16) & 0xFF;
+                    int newGreen = (newPixel >>>  8) & 0xFF;
+                    int newBlue  =  newPixel         & 0xFF;
+
+                    int redError   = (oldRed - newRed) / 16;
+                    int greenError = (oldGreen - newGreen) / 16;
+                    int blueError  = (oldBlue - newBlue) / 16;
+
+                    int red, green, blue;
+                    if (imageX < image.getWidth() - 1) {
+                        int pXpY  = ditheredImage.getRGB(imageX + 1, imageY);
+                        red   = (int) ((pXpY >>> 16) & 0xFF) + (7 * redError);
+                        green = (int) ((pXpY >>>  8) & 0xFF) + (7 * greenError);
+                        blue  = (int) ( pXpY         & 0xFF) + (7 * blueError);
+                        red = clamp(red);
+                        green = clamp(green);
+                        blue = clamp(blue);
+                        pXpY = ((red & 0xFF) << 16);
+                        pXpY |= ((green & 0xFF) << 8) | (blue & 0xFF);
+                        ditheredImage.setRGB(imageX + 1, imageY, pXpY);
+
+                        if (imageY < image.getHeight() - 1) {
+                            int pXpYp = ditheredImage.getRGB(imageX + 1,
+                                imageY + 1);
+                            red   = (int) ((pXpYp >>> 16) & 0xFF) + redError;
+                            green = (int) ((pXpYp >>>  8) & 0xFF) + greenError;
+                            blue  = (int) ( pXpYp         & 0xFF) + blueError;
+                            red = clamp(red);
+                            green = clamp(green);
+                            blue = clamp(blue);
+                            pXpYp = ((red & 0xFF) << 16);
+                            pXpYp |= ((green & 0xFF) << 8) | (blue & 0xFF);
+                            ditheredImage.setRGB(imageX + 1, imageY + 1, pXpYp);
+                        }
+                    } else if (imageY < image.getHeight() - 1) {
+                        int pXmYp = ditheredImage.getRGB(imageX - 1,
+                            imageY + 1);
+                        int pXYp  = ditheredImage.getRGB(imageX,
+                            imageY + 1);
+
+                        red   = (int) ((pXmYp >>> 16) & 0xFF) + (3 * redError);
+                        green = (int) ((pXmYp >>>  8) & 0xFF) + (3 * greenError);
+                        blue  = (int) ( pXmYp         & 0xFF) + (3 * blueError);
+                        red = clamp(red);
+                        green = clamp(green);
+                        blue = clamp(blue);
+                        pXmYp = ((red & 0xFF) << 16);
+                        pXmYp |= ((green & 0xFF) << 8) | (blue & 0xFF);
+                        ditheredImage.setRGB(imageX - 1, imageY + 1, pXmYp);
+
+                        red   = (int) ((pXYp >>> 16) & 0xFF) + (5 * redError);
+                        green = (int) ((pXYp >>>  8) & 0xFF) + (5 * greenError);
+                        blue  = (int) ( pXYp         & 0xFF) + (5 * blueError);
+                        red = clamp(red);
+                        green = clamp(green);
+                        blue = clamp(blue);
+                        pXYp = ((red & 0xFF) << 16);
+                        pXYp |= ((green & 0xFF) << 8) | (blue & 0xFF);
+                        ditheredImage.setRGB(imageX,     imageY + 1, pXYp);
+                    }
+                } // for (int imageY = 0; imageY < image.getHeight(); imageY++)
+            } // for (int imageX = 0; imageX < image.getWidth(); imageX++)
+
+            return ditheredImage;
+        }
+
+        /**
+         * Convert an RGB color to HSL.
+         *
+         * @param red red color, between 0 and 255
+         * @param green green color, between 0 and 255
+         * @param blue blue color, between 0 and 255
+         * @param hsl the hsl color as [hue, saturation, luminance]
+         */
+        private void rgbToHsl(final int red, final int green,
+            final int blue, final int [] hsl) {
+
+            assert ((red >= 0) && (red <= 255));
+            assert ((green >= 0) && (green <= 255));
+            assert ((blue >= 0) && (blue <= 255));
+
+            double R = red / 255.0;
+            double G = green / 255.0;
+            double B = blue / 255.0;
+            boolean Rmax = false;
+            boolean Gmax = false;
+            boolean Bmax = false;
+            double min = (R < G ? R : G);
+            min = (min < B ? min : B);
+            double max = 0;
+            if ((R >= G) && (R >= B)) {
+                max = R;
+                Rmax = true;
+            } else if ((G >= R) && (G >= B)) {
+                max = G;
+                Gmax = true;
+            } else if ((B >= G) && (B >= R)) {
+                max = B;
+                Bmax = true;
+            }
+
+            double L = (min + max) / 2.0;
+            double H = 0.0;
+            double S = 0.0;
+            if (min != max) {
+                if (L < 0.5) {
+                    S = (max - min) / (max + min);
+                } else {
+                    S = (max - min) / (2.0 - max - min);
+                }
+            }
+            if (Rmax) {
+                assert (Gmax == false);
+                assert (Bmax == false);
+                H = (G - B) / (max - min);
+            } else if (Gmax) {
+                assert (Rmax == false);
+                assert (Bmax == false);
+                H = 2.0 + (B - R) / (max - min);
+            } else if (Bmax) {
+                assert (Rmax == false);
+                assert (Gmax == false);
+                H = 4.0 + (R - G) / (max - min);
+            }
+            if (H < 0.0) {
+                H += 6.0;
+            }
+            hsl[0] = (int) (H * 60.0);
+            hsl[1] = (int) (S * 100.0);
+            hsl[2] = (int) (L * 100.0);
+
+            assert ((hsl[0] >= 0) && (hsl[0] <= 360));
+            assert ((hsl[1] >= 0) && (hsl[1] <= 100));
+            assert ((hsl[2] >= 0) && (hsl[2] <= 100));
+        }
+
+        /**
+         * Convert a HSL color to RGB.
+         *
+         * @param hue hue, between 0 and 359
+         * @param sat saturation, between 0 and 100
+         * @param lum luminance, between 0 and 100
+         * @return the rgb color as 0x00RRGGBB
+         */
+        private int hslToRgb(final int hue, final int sat, final int lum) {
+            assert ((hue >= 0) && (hue <= 360));
+            assert ((sat >= 0) && (sat <= 100));
+            assert ((lum >= 0) && (lum <= 100));
+
+            double S = sat / 100.0;
+            double L = lum / 100.0;
+            double C = (1.0 - Math.abs((2.0 * L) - 1.0)) * S;
+            double Hp = hue / 60.0;
+            double X = C * (1.0 - Math.abs((Hp % 2) - 1.0));
+            double Rp = 0.0;
+            double Gp = 0.0;
+            double Bp = 0.0;
+            if (Hp <= 1.0) {
+                Rp = C;
+                Gp = X;
+            } else if (Hp <= 2.0) {
+                Rp = X;
+                Gp = C;
+            } else if (Hp <= 3.0) {
+                Gp = C;
+                Bp = X;
+            } else if (Hp <= 4.0) {
+                Gp = X;
+                Bp = C;
+            } else if (Hp <= 5.0) {
+                Rp = X;
+                Bp = C;
+            } else if (Hp <= 6.0) {
+                Rp = C;
+                Bp = X;
+            }
+            double m = L - (C / 2.0);
+            int red   = ((int) ((Rp + m) * 255.0)) << 16;
+            int green = ((int) ((Gp + m) * 255.0)) << 8;
+            int blue  =  (int) ((Bp + m) * 255.0);
+
+            return (red | green | blue);
+        }
+
+        /**
+         * Create the sixel palette.
+         */
+        private void makePalette() {
+            // Generate the sixel palette.  Because we have no idea at this
+            // layer which image(s) will be shown, we have to use a common
+            // palette with MAX_COLOR_REGISTERS colors for everything, and
+            // map the BufferedImage colors to their nearest neighbor in RGB
+            // space.
+
+            // We build a palette using the Hue-Saturation-Luminence model,
+            // with 5+ bits for Hue, 2+ bits for Saturation, and 1+ bit for
+            // Luminance.  We convert these colors to 24-bit RGB, sort them
+            // ascending, and steal the first index for pure black and the
+            // last for pure white.  The 8-bit final palette favors bright
+            // colors, somewhere between pastel and classic television
+            // technicolor.  9- and 10-bit palettes are more uniform.
+
+            // Default at 256 colors.
+            hueBits = 5;
+            satBits = 2;
+            lumBits = 1;
+
+            assert (MAX_COLOR_REGISTERS >= 256);
+            assert ((MAX_COLOR_REGISTERS == 256)
+                || (MAX_COLOR_REGISTERS == 512)
+                || (MAX_COLOR_REGISTERS == 1024)
+                || (MAX_COLOR_REGISTERS == 2048));
+
+            switch (MAX_COLOR_REGISTERS) {
+            case 512:
+                hueBits = 5;
+                satBits = 2;
+                lumBits = 2;
+                break;
+            case 1024:
+                hueBits = 5;
+                satBits = 2;
+                lumBits = 3;
+                break;
+            case 2048:
+                hueBits = 5;
+                satBits = 3;
+                lumBits = 3;
+                break;
+            }
+            hueStep = (int) (Math.pow(2, hueBits));
+            satStep = (int) (100 / Math.pow(2, satBits));
+            // 1 bit for luminance: 40 and 70.
+            int lumBegin = 40;
+            int lumStep = 30;
+            switch (lumBits) {
+            case 2:
+                // 2 bits: 20, 40, 60, 80
+                lumBegin = 20;
+                lumStep = 20;
+                break;
+            case 3:
+                // 3 bits: 8, 20, 32, 44, 56, 68, 80, 92
+                lumBegin = 8;
+                lumStep = 12;
+                break;
+            }
+
+            // System.err.printf("<html><body>\n");
+            // Hue is evenly spaced around the wheel.
+            hslColors = new ArrayList<ArrayList<ArrayList<ColorIdx>>>();
+
+            final boolean DEBUG = false;
+            ArrayList<Integer> rawRgbList = new ArrayList<Integer>();
+
+            for (int hue = 0; hue < (360 - (360 % hueStep));
+                 hue += (360/hueStep)) {
+
+                ArrayList<ArrayList<ColorIdx>> satList = null;
+                satList = new ArrayList<ArrayList<ColorIdx>>();
+                hslColors.add(satList);
+
+                // Saturation is linearly spaced between pastel and pure.
+                for (int sat = satStep; sat <= 100; sat += satStep) {
+
+                    ArrayList<ColorIdx> lumList = new ArrayList<ColorIdx>();
+                    satList.add(lumList);
+
+                    // Luminance brackets the pure color, but leaning toward
+                    // lighter.
+                    for (int lum = lumBegin; lum < 100; lum += lumStep) {
+                        /*
+                        System.err.printf("<font style = \"color:");
+                        System.err.printf("hsl(%d, %d%%, %d%%)",
+                            hue, sat, lum);
+                        System.err.printf(";\">=</font>\n");
+                        */
+                        int rgbColor = hslToRgb(hue, sat, lum);
+                        rgbColors.add(rgbColor);
+                        ColorIdx colorIdx = new ColorIdx(rgbColor,
+                            rgbColors.size() - 1);
+                        lumList.add(colorIdx);
+
+                        rawRgbList.add(rgbColor);
+                        if (DEBUG) {
+                            int red   = (rgbColor >>> 16) & 0xFF;
+                            int green = (rgbColor >>>  8) & 0xFF;
+                            int blue  =  rgbColor         & 0xFF;
+                            int [] backToHsl = new int[3];
+                            rgbToHsl(red, green, blue, backToHsl);
+                            System.err.printf("%d [%d] %d [%d] %d [%d]\n",
+                                hue, backToHsl[0], sat, backToHsl[1],
+                                lum, backToHsl[2]);
+                        }
+                    }
+                }
+            }
+            // System.err.printf("\n</body></html>\n");
+
+            assert (rgbColors.size() == MAX_COLOR_REGISTERS);
+
+            /*
+             * We need to sort rgbColors, so that toSixel() can know where
+             * BLACK and WHITE are in it.  But we also need to be able to
+             * find the sorted values using the old unsorted indexes.  So we
+             * will sort it, put all the indexes into a HashMap, and then
+             * build rgbSortedIndex[].
+             */
+            Collections.sort(rgbColors);
+            HashMap<Integer, Integer> rgbColorIndices = null;
+            rgbColorIndices = new HashMap<Integer, Integer>();
+            for (int i = 0; i < MAX_COLOR_REGISTERS; i++) {
+                rgbColorIndices.put(rgbColors.get(i), i);
+            }
+            for (int i = 0; i < MAX_COLOR_REGISTERS; i++) {
+                int rawColor = rawRgbList.get(i);
+                rgbSortedIndex[i] = rgbColorIndices.get(rawColor);
+            }
+            if (DEBUG) {
+                for (int i = 0; i < MAX_COLOR_REGISTERS; i++) {
+                    assert (rawRgbList != null);
+                    int idx = rgbSortedIndex[i];
+                    int rgbColor = rgbColors.get(idx);
+                    if ((idx != 0) && (idx != MAX_COLOR_REGISTERS - 1)) {
+                        /*
+                        System.err.printf("%d %06x --> %d %06x\n",
+                            i, rawRgbList.get(i), idx, rgbColors.get(idx));
+                        */
+                        assert (rgbColor == rawRgbList.get(i));
+                    }
+                }
+            }
+
+            // Set the dimmest color as true black, and the brightest as true
+            // white.
+            rgbColors.set(0, 0);
+            rgbColors.set(MAX_COLOR_REGISTERS - 1, 0xFFFFFF);
+
+            /*
+            System.err.printf("<html><body>\n");
+            for (Integer rgb: rgbColors) {
+                System.err.printf("<font style = \"color:");
+                System.err.printf("#%06x", rgb);
+                System.err.printf(";\">=</font>\n");
+            }
+            System.err.printf("\n</body></html>\n");
+            */
+
+        }
+
+        /**
+         * Emit the sixel palette.
+         *
+         * @param sb the StringBuilder to append to
+         * @param used array of booleans set to true for each color actually
+         * used in this cell, or null to emit the entire palette
+         * @return the string to emit to an ANSI / ECMA-style terminal
+         */
+        public String emitPalette(final StringBuilder sb,
+            final boolean [] used) {
+
+            for (int i = 0; i < MAX_COLOR_REGISTERS; i++) {
+                if (((used != null) && (used[i] == true)) || (used == null)) {
+                    int rgbColor = rgbColors.get(i);
+                    sb.append(String.format("#%d;2;%d;%d;%d", i,
+                            ((rgbColor >>> 16) & 0xFF) * 100 / 255,
+                            ((rgbColor >>>  8) & 0xFF) * 100 / 255,
+                            ( rgbColor         & 0xFF) * 100 / 255));
+                }
+            }
+            return sb.toString();
+        }
+    }
+
+    /**
+     * SixelCache is a least-recently-used cache that hangs on to the
+     * post-rendered sixel string for a particular set of cells.
+     */
+    private class SixelCache {
+
+        /**
+         * Maximum size of the cache.
+         */
+        private int maxSize = 100;
+
+        /**
+         * The entries stored in the cache.
+         */
+        private HashMap<String, CacheEntry> cache = null;
+
+        /**
+         * CacheEntry is one entry in the cache.
+         */
+        private class CacheEntry {
+            /**
+             * The cache key.
+             */
+            public String key;
+
+            /**
+             * The cache data.
+             */
+            public String data;
+
+            /**
+             * The last time this entry was used.
+             */
+            public long millis = 0;
+
+            /**
+             * Public constructor.
+             *
+             * @param key the cache entry key
+             * @param data the cache entry data
+             */
+            public CacheEntry(final String key, final String data) {
+                this.key = key;
+                this.data = data;
+                this.millis = System.currentTimeMillis();
+            }
+        }
+
+        /**
+         * Public constructor.
+         *
+         * @param maxSize the maximum size of the cache
+         */
+        public SixelCache(final int maxSize) {
+            this.maxSize = maxSize;
+            cache = new HashMap<String, CacheEntry>();
+        }
+
+        /**
+         * Make a unique key for a list of cells.
+         *
+         * @param cells the cells
+         * @return the key
+         */
+        private String makeKey(final ArrayList<Cell> cells) {
+            StringBuilder sb = new StringBuilder();
+            for (Cell cell: cells) {
+                sb.append(cell.hashCode());
+            }
+            return sb.toString();
+        }
+
+        /**
+         * Get an entry from the cache.
+         *
+         * @param cells the list of cells that are the cache key
+         * @return the sixel string representing these cells, or null if this
+         * list of cells is not in the cache
+         */
+        public String get(final ArrayList<Cell> cells) {
+            CacheEntry entry = cache.get(makeKey(cells));
+            if (entry == null) {
+                return null;
+            }
+            entry.millis = System.currentTimeMillis();
+            return entry.data;
+        }
+
+        /**
+         * Put an entry into the cache.
+         *
+         * @param cells the list of cells that are the cache key
+         * @param data the sixel string representing these cells
+         */
+        public void put(final ArrayList<Cell> cells, final String data) {
+            String key = makeKey(cells);
+
+            // System.err.println("put() " + key + " size " + cache.size());
+
+            assert (!cache.containsKey(key));
+
+            assert (cache.size() <= maxSize);
+            if (cache.size() == maxSize) {
+                // Cache is at limit, evict oldest entry.
+                long oldestTime = Long.MAX_VALUE;
+                String keyToRemove = null;
+                for (CacheEntry entry: cache.values()) {
+                    if ((entry.millis < oldestTime) || (keyToRemove == null)) {
+                        keyToRemove = entry.key;
+                        oldestTime = entry.millis;
+                    }
+                }
+                /*
+                System.err.println("put() remove key = " + keyToRemove +
+                    " size " + cache.size());
+                 */
+                assert (keyToRemove != null);
+                cache.remove(keyToRemove);
+                /*
+                System.err.println("put() removed, size " + cache.size());
+                 */
+            }
+            assert (cache.size() <= maxSize);
+            CacheEntry entry = new CacheEntry(key, data);
+            assert (key.equals(entry.key));
+            cache.put(key, entry);
+            /*
+            System.err.println("put() added key " + key + " " +
+                " size " + cache.size());
+             */
+        }
+
+    }
+
     // ------------------------------------------------------------------------
     // Constructors -----------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -286,6 +1069,9 @@ public class ECMA48Terminal extends LogicalScreen
                     "UTF-8"));
         }
 
+        // Request xterm report window dimensions in pixels
+        this.output.printf("%s", xtermReportWindowPixelDimensions());
+
         // Enable mouse reporting and metaSendsEscape
         this.output.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
         this.output.flush();
@@ -299,7 +1085,7 @@ public class ECMA48Terminal extends LogicalScreen
         windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN,
             sessionInfo.getWindowWidth(), sessionInfo.getWindowHeight());
 
-        // Permit RGB colors only if externally requested
+        // Permit RGB colors only if externally requested.
         if (System.getProperty("jexer.ECMA48.rgbColor") != null) {
             if (System.getProperty("jexer.ECMA48.rgbColor").equals("true")) {
                 doRgbColor = true;
@@ -308,6 +1094,15 @@ public class ECMA48Terminal extends LogicalScreen
             }
         }
 
+        // Pull the system properties for sixel output.
+        if (System.getProperty("jexer.ECMA48.sixel") != null) {
+            if (System.getProperty("jexer.ECMA48.sixel").equals("true")) {
+                sixel = true;
+            } else {
+                sixel = false;
+            }
+        }
+
         // Spin up the input reader
         eventQueue = new LinkedList<TInputEvent>();
         readerThread = new Thread(this);
@@ -376,6 +1171,9 @@ public class ECMA48Terminal extends LogicalScreen
 
         this.output = writer;
 
+        // Request xterm report window dimensions in pixels
+        this.output.printf("%s", xtermReportWindowPixelDimensions());
+
         // Enable mouse reporting and metaSendsEscape
         this.output.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
         this.output.flush();
@@ -398,6 +1196,15 @@ public class ECMA48Terminal extends LogicalScreen
             }
         }
 
+        // Pull the system properties for sixel output.
+        if (System.getProperty("jexer.ECMA48.sixel") != null) {
+            if (System.getProperty("jexer.ECMA48.sixel").equals("true")) {
+                sixel = true;
+            } else {
+                sixel = false;
+            }
+        }
+
         // Spin up the input reader
         eventQueue = new LinkedList<TInputEvent>();
         readerThread = new Thread(this);
@@ -445,19 +1252,21 @@ public class ECMA48Terminal extends LogicalScreen
      */
     @Override
     public void flushPhysical() {
-        String result = flushString();
+        StringBuilder sb = new StringBuilder();
         if ((cursorVisible)
             && (cursorY >= 0)
             && (cursorX >= 0)
             && (cursorY <= height - 1)
             && (cursorX <= width - 1)
         ) {
-            result += cursor(true);
-            result += gotoXY(cursorX, cursorY);
+            flushString(sb);
+            sb.append(cursor(true));
+            sb.append(gotoXY(cursorX, cursorY));
         } else {
-            result += cursor(false);
+            sb.append(cursor(false));
+            flushString(sb);
         }
-        output.write(result);
+        output.write(sb.toString());
         flush();
     }
 
@@ -504,7 +1313,9 @@ public class ECMA48Terminal extends LogicalScreen
         try {
             readerThread.join();
         } catch (InterruptedException e) {
-            e.printStackTrace();
+            if (debugToStderr) {
+                e.printStackTrace();
+            }
         }
 
         // Disable mouse reporting and show cursor.  Defensive null check
@@ -521,17 +1332,17 @@ public class ECMA48Terminal extends LogicalScreen
         } else {
             // Shut down the streams, this should wake up the reader thread
             // and make it exit.
-            try {
-                if (input != null) {
+            if (input != null) {
+                try {
                     input.close();
-                    input = null;
-                }
-                if (output != null) {
-                    output.close();
-                    output = null;
+                } catch (IOException e) {
+                    // SQUASH
                 }
-            } catch (IOException e) {
-                e.printStackTrace();
+                input = null;
+            }
+            if (output != null) {
+                output.close();
+                output = null;
             }
         }
     }
@@ -641,6 +1452,24 @@ public class ECMA48Terminal extends LogicalScreen
     // ECMA48Terminal ---------------------------------------------------------
     // ------------------------------------------------------------------------
 
+    /**
+     * Get the width of a character cell in pixels.
+     *
+     * @return the width in pixels of a character cell
+     */
+    public int getTextWidth() {
+        return (widthPixels / sessionInfo.getWindowWidth());
+    }
+
+    /**
+     * Get the height of a character cell in pixels.
+     *
+     * @return the height in pixels of a character cell
+     */
+    public int getTextHeight() {
+        return (heightPixels / sessionInfo.getWindowHeight());
+    }
+
     /**
      * Getter for sessionInfo.
      *
@@ -713,7 +1542,9 @@ public class ECMA48Terminal extends LogicalScreen
                     process.waitFor();
                     break;
                 } catch (InterruptedException e) {
-                    e.printStackTrace();
+                    if (debugToStderr) {
+                        e.printStackTrace();
+                    }
                 }
             }
             int rc = process.exitValue();
@@ -756,6 +1587,8 @@ public class ECMA48Terminal extends LogicalScreen
         // DEBUG
         // reallyCleared = true;
 
+        boolean hasImage = false;
+
         for (int x = 0; x < width; x++) {
             Cell lCell = logical[x][y];
             Cell pCell = physical[x][y];
@@ -798,6 +1631,25 @@ public class ECMA48Terminal extends LogicalScreen
                     return;
                 }
 
+                // Image cell: bypass the rest of the loop, it is not
+                // rendered here.
+                if (lCell.isImage()) {
+                    hasImage = true;
+
+                    // Save the last rendered cell
+                    lastX = x;
+
+                    // Physical is always updated
+                    physical[x][y].setTo(lCell);
+                    continue;
+                }
+
+                assert (!lCell.isImage());
+                if (hasImage) {
+                    hasImage = false;
+                    sb.append(gotoXY(x, y));
+                }
+
                 // Now emit only the modified attributes
                 if ((lCell.getForeColor() != lastAttr.getForeColor())
                     && (lCell.getBackColor() != lastAttr.getBackColor())
@@ -970,18 +1822,70 @@ public class ECMA48Terminal extends LogicalScreen
      * Render the screen to a string that can be emitted to something that
      * knows how to process ECMA-48/ANSI X3.64 escape sequences.
      *
+     * @param sb StringBuilder to write escape sequences to
      * @return escape sequences string that provides the updates to the
      * physical screen
      */
-    private String flushString() {
+    private String flushString(final StringBuilder sb) {
         CellAttributes attr = null;
 
-        StringBuilder sb = new StringBuilder();
         if (reallyCleared) {
             attr = new CellAttributes();
             sb.append(clearAll());
         }
 
+        /*
+         * For sixel support, draw all of the sixel output first, and then
+         * draw everything else afterwards.  This works OK, but performance
+         * is still a drag on larger pictures.
+         */
+        for (int y = 0; y < height; y++) {
+            for (int x = 0; x < width; x++) {
+                // If physical had non-image data that is now image data, the
+                // entire row must be redrawn.
+                Cell lCell = logical[x][y];
+                Cell pCell = physical[x][y];
+                if (lCell.isImage() && !pCell.isImage()) {
+                    unsetImageRow(y);
+                    break;
+                }
+            }
+        }
+        for (int y = 0; y < height; y++) {
+            for (int x = 0; x < width; x++) {
+                Cell lCell = logical[x][y];
+                Cell pCell = physical[x][y];
+
+                if (!lCell.isImage()) {
+                    continue;
+                }
+
+                int left = x;
+                int right = x;
+                while ((right < width)
+                    && (logical[right][y].isImage())
+                    && (!logical[right][y].equals(physical[right][y])
+                        || reallyCleared)
+                ) {
+                    right++;
+                }
+                ArrayList<Cell> cellsToDraw = new ArrayList<Cell>();
+                for (int i = 0; i < (right - x); i++) {
+                    assert (logical[x + i][y].isImage());
+                    cellsToDraw.add(logical[x + i][y]);
+
+                    // Physical is always updated.
+                    physical[x + i][y].setTo(lCell);
+                }
+                if (cellsToDraw.size() > 0) {
+                    sb.append(toSixel(x, y, cellsToDraw));
+                }
+
+                x = right;
+            }
+        }
+
+        // Draw the text part now.
         for (int y = 0; y < height; y++) {
             flushLine(y, sb, attr);
         }
@@ -1335,6 +2239,10 @@ public class ECMA48Terminal extends LogicalScreen
                         newWidth + " x " + newHeight);
                 }
 
+                // Request xterm report window dimensions in pixels again.
+                this.output.printf("%s", xtermReportWindowPixelDimensions());
+                this.output.flush();
+
                 TResizeEvent event = new TResizeEvent(TResizeEvent.Type.SCREEN,
                     newWidth, newHeight);
                 windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN,
@@ -1697,6 +2605,31 @@ public class ECMA48Terminal extends LogicalScreen
                     events.add(new TKeypressEvent(kbEnd, alt, ctrl, shift));
                     resetParser();
                     return;
+                case 't':
+                    // windowOps
+                    if ((params.size() > 2) && (params.get(0).equals("4"))) {
+                        if (debugToStderr) {
+                            System.err.printf("windowOp pixels: " +
+                                "height %s width %s\n",
+                                params.get(1), params.get(2));
+                        }
+                        try {
+                            widthPixels = Integer.parseInt(params.get(2));
+                            heightPixels = Integer.parseInt(params.get(1));
+                        } catch (NumberFormatException e) {
+                            if (debugToStderr) {
+                                e.printStackTrace();
+                            }
+                        }
+                        if (widthPixels <= 0) {
+                            widthPixels = 640;
+                        }
+                        if (heightPixels <= 0) {
+                            heightPixels = 400;
+                        }
+                    }
+                    resetParser();
+                    return;
                 default:
                     break;
                 }
@@ -1723,6 +2656,15 @@ public class ECMA48Terminal extends LogicalScreen
         return;
     }
 
+    /**
+     * Request (u)xterm to report the current window size dimensions.
+     *
+     * @return the string to emit to xterm
+     */
+    private String xtermReportWindowPixelDimensions() {
+        return "\033[14t";
+    }
+
     /**
      * Tell (u)xterm that we want alt- keystrokes to send escape + character
      * rather than set the 8th bit.  Anyone who wants UTF8 should want this
@@ -1748,6 +2690,264 @@ public class ECMA48Terminal extends LogicalScreen
         return "\033]2;" + title + "\007";
     }
 
+    // ------------------------------------------------------------------------
+    // Sixel output support ---------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Start a sixel string for display one row's worth of bitmap data.
+     *
+     * @param x column coordinate.  0 is the left-most column.
+     * @param y row coordinate.  0 is the top-most row.
+     * @return the string to emit to an ANSI / ECMA-style terminal
+     */
+    private String startSixel(final int x, final int y) {
+        StringBuilder sb = new StringBuilder();
+
+        assert (sixel == true);
+
+        // Place the cursor
+        sb.append(gotoXY(x, y));
+
+        // DCS
+        sb.append("\033Pq");
+
+        if (palette == null) {
+            palette = new SixelPalette();
+        }
+
+        return sb.toString();
+    }
+
+    /**
+     * End a sixel string for display one row's worth of bitmap data.
+     *
+     * @return the string to emit to an ANSI / ECMA-style terminal
+     */
+    private String endSixel() {
+        assert (sixel == true);
+
+        // ST
+        return ("\033\\");
+    }
+
+    /**
+     * Create a sixel string representing a row of several cells containing
+     * bitmap data.
+     *
+     * @param x column coordinate.  0 is the left-most column.
+     * @param y row coordinate.  0 is the top-most row.
+     * @param cells the cells containing the bitmap data
+     * @return the string to emit to an ANSI / ECMA-style terminal
+     */
+    private String toSixel(final int x, final int y,
+        final ArrayList<Cell> cells) {
+
+        StringBuilder sb = new StringBuilder();
+
+        assert (sixel == true);
+        assert (cells != null);
+        assert (cells.size() > 0);
+        assert (cells.get(0).getImage() != null);
+
+        if (sixelCache == null) {
+            sixelCache = new SixelCache(height * 10);
+        }
+
+        // Save and get rows to/from the cache that do NOT have inverted
+        // cells.
+        boolean saveInCache = true;
+        for (Cell cell: cells) {
+            if (cell.isInvertedImage()) {
+                saveInCache = false;
+            }
+        }
+        if (saveInCache) {
+            String cachedResult = sixelCache.get(cells);
+            if (cachedResult != null) {
+                // System.err.println("CACHE HIT");
+                sb.append(startSixel(x, y));
+                sb.append(cachedResult);
+                sb.append(endSixel());
+                return sb.toString();
+            }
+            // System.err.println("CACHE MISS");
+        }
+
+        int imageWidth = cells.get(0).getImage().getWidth();
+        int imageHeight = cells.get(0).getImage().getHeight();
+
+        // cells.get(x).getImage() has a dithered bitmap containing indexes
+        // into the color palette.  Piece these together into one larger
+        // image for final rendering.
+        int totalWidth = 0;
+        int fullWidth = cells.size() * getTextWidth();
+        int fullHeight = getTextHeight();
+        for (int i = 0; i < cells.size(); i++) {
+            totalWidth += cells.get(i).getImage().getWidth();
+        }
+
+        BufferedImage image = new BufferedImage(fullWidth,
+            fullHeight, BufferedImage.TYPE_INT_ARGB);
+
+        int [] rgbArray;
+        for (int i = 0; i < cells.size() - 1; i++) {
+            if (cells.get(i).isInvertedImage()) {
+                rgbArray = new int[imageWidth * imageHeight];
+                for (int j = 0; j < rgbArray.length; j++) {
+                    rgbArray[j] = 0xFFFFFF;
+                }
+            } else {
+                rgbArray = cells.get(i).getImage().getRGB(0, 0,
+                    imageWidth, imageHeight, null, 0, imageWidth);
+            }
+            image.setRGB(i * imageWidth, 0, imageWidth, imageHeight,
+                rgbArray, 0, imageWidth);
+            if (imageHeight < fullHeight) {
+                int backgroundColor = cells.get(i).getBackground().getRGB();
+                for (int imageX = 0; imageX < image.getWidth(); imageX++) {
+                    for (int imageY = imageHeight; imageY < fullHeight;
+                         imageY++) {
+
+                        image.setRGB(imageX, imageY, backgroundColor);
+                    }
+                }
+            }
+        }
+        totalWidth -= ((cells.size() - 1) * imageWidth);
+        if (cells.get(cells.size() - 1).isInvertedImage()) {
+            rgbArray = new int[totalWidth * imageHeight];
+            for (int j = 0; j < rgbArray.length; j++) {
+                rgbArray[j] = 0xFFFFFF;
+            }
+        } else {
+            rgbArray = cells.get(cells.size() - 1).getImage().getRGB(0, 0,
+                totalWidth, imageHeight, null, 0, totalWidth);
+        }
+        image.setRGB((cells.size() - 1) * imageWidth, 0, totalWidth,
+            imageHeight, rgbArray, 0, totalWidth);
+
+        if (totalWidth < getTextWidth()) {
+            int backgroundColor = cells.get(cells.size() - 1).getBackground().getRGB();
+
+            for (int imageX = image.getWidth() - totalWidth;
+                 imageX < image.getWidth(); imageX++) {
+
+                for (int imageY = 0; imageY < fullHeight; imageY++) {
+                    image.setRGB(imageX, imageY, backgroundColor);
+                }
+            }
+        }
+
+        // Dither the image.  It is ok to lose the original here.
+        if (palette == null) {
+            palette = new SixelPalette();
+        }
+        image = palette.ditherImage(image);
+
+        // Emit the palette, but only for the colors actually used by these
+        // cells.
+        boolean [] usedColors = new boolean[MAX_COLOR_REGISTERS];
+        for (int imageX = 0; imageX < image.getWidth(); imageX++) {
+            for (int imageY = 0; imageY < image.getHeight(); imageY++) {
+                usedColors[image.getRGB(imageX, imageY)] = true;
+            }
+        }
+        palette.emitPalette(sb, usedColors);
+
+        // Render the entire row of cells.
+        for (int currentRow = 0; currentRow < fullHeight; currentRow += 6) {
+            int [][] sixels = new int[image.getWidth()][6];
+
+            // See which colors are actually used in this band of sixels.
+            for (int imageX = 0; imageX < image.getWidth(); imageX++) {
+                for (int imageY = 0;
+                     (imageY < 6) && (imageY + currentRow < fullHeight);
+                     imageY++) {
+
+                    int colorIdx = image.getRGB(imageX, imageY + currentRow);
+                    assert (colorIdx >= 0);
+                    assert (colorIdx < MAX_COLOR_REGISTERS);
+
+                    sixels[imageX][imageY] = colorIdx;
+                }
+            }
+
+            for (int i = 0; i < MAX_COLOR_REGISTERS; i++) {
+                boolean isUsed = false;
+                for (int imageX = 0; imageX < image.getWidth(); imageX++) {
+                    for (int j = 0; j < 6; j++) {
+                        if (sixels[imageX][j] == i) {
+                            isUsed = true;
+                        }
+                    }
+                }
+                if (isUsed == false) {
+                    continue;
+                }
+
+                // Set to the beginning of scan line for the next set of
+                // colored pixels, and select the color.
+                sb.append(String.format("$#%d", i));
+
+                for (int imageX = 0; imageX < image.getWidth(); imageX++) {
+
+                    // Add up all the pixels that match this color.
+                    int data = 0;
+                    for (int j = 0;
+                         (j < 6) && (currentRow + j < fullHeight);
+                         j++) {
+
+                        if (sixels[imageX][j] == i) {
+                            switch (j) {
+                            case 0:
+                                data += 1;
+                                break;
+                            case 1:
+                                data += 2;
+                                break;
+                            case 2:
+                                data += 4;
+                                break;
+                            case 3:
+                                data += 8;
+                                break;
+                            case 4:
+                                data += 16;
+                                break;
+                            case 5:
+                                data += 32;
+                                break;
+                            }
+                        }
+                    }
+                    assert (data >= 0);
+                    assert (data < 127);
+                    data += 63;
+                    sb.append((char) data);
+                } // for (int imageX = 0; imageX < image.getWidth(); imageX++)
+            } // for (int i = 0; i < MAX_COLOR_REGISTERS; i++)
+
+            // Advance to the next scan line.
+            sb.append("-");
+
+        } // for (int currentRow = 0; currentRow < imageHeight; currentRow += 6)
+
+        // Kill the very last "-", because it is unnecessary.
+        sb.deleteCharAt(sb.length() - 1);
+
+        if (saveInCache) {
+            // This row is OK to save into the cache.
+            sixelCache.put(cells, sb.toString());
+        }
+
+        return (startSixel(x, y) + sb.toString() + endSixel());
+    }
+
+    // ------------------------------------------------------------------------
+    // End sixel output support -----------------------------------------------
+    // ------------------------------------------------------------------------
+
     /**
      * Create a SGR parameter sequence for a single color change.
      *
@@ -1773,9 +2973,9 @@ public class ECMA48Terminal extends LogicalScreen
      */
     private String colorRGB(final int colorRGB, final boolean foreground) {
 
-        int colorRed     = (colorRGB >> 16) & 0xFF;
-        int colorGreen   = (colorRGB >>  8) & 0xFF;
-        int colorBlue    =  colorRGB        & 0xFF;
+        int colorRed     = (colorRGB >>> 16) & 0xFF;
+        int colorGreen   = (colorRGB >>>  8) & 0xFF;
+        int colorBlue    =  colorRGB         & 0xFF;
 
         StringBuilder sb = new StringBuilder();
         if (foreground) {
@@ -1797,12 +2997,12 @@ public class ECMA48Terminal extends LogicalScreen
      * e.g. "\033[42m"
      */
     private String colorRGB(final int foreColorRGB, final int backColorRGB) {
-        int foreColorRed     = (foreColorRGB >> 16) & 0xFF;
-        int foreColorGreen   = (foreColorRGB >>  8) & 0xFF;
-        int foreColorBlue    =  foreColorRGB        & 0xFF;
-        int backColorRed     = (backColorRGB >> 16) & 0xFF;
-        int backColorGreen   = (backColorRGB >>  8) & 0xFF;
-        int backColorBlue    =  backColorRGB        & 0xFF;
+        int foreColorRed     = (foreColorRGB >>> 16) & 0xFF;
+        int foreColorGreen   = (foreColorRGB >>>  8) & 0xFF;
+        int foreColorBlue    =  foreColorRGB         & 0xFF;
+        int backColorRed     = (backColorRGB >>> 16) & 0xFF;
+        int backColorGreen   = (backColorRGB >>>  8) & 0xFF;
+        int backColorBlue    =  backColorRGB         & 0xFF;
 
         StringBuilder sb = new StringBuilder();
         sb.append(String.format("\033[38;2;%d;%d;%dm",
@@ -2051,12 +3251,12 @@ public class ECMA48Terminal extends LogicalScreen
         final boolean bold, final boolean reverse, final boolean blink,
         final boolean underline) {
 
-        int foreColorRed     = (foreColorRGB >> 16) & 0xFF;
-        int foreColorGreen   = (foreColorRGB >>  8) & 0xFF;
-        int foreColorBlue    =  foreColorRGB        & 0xFF;
-        int backColorRed     = (backColorRGB >> 16) & 0xFF;
-        int backColorGreen   = (backColorRGB >>  8) & 0xFF;
-        int backColorBlue    =  backColorRGB        & 0xFF;
+        int foreColorRed     = (foreColorRGB >>> 16) & 0xFF;
+        int foreColorGreen   = (foreColorRGB >>>  8) & 0xFF;
+        int foreColorBlue    =  foreColorRGB         & 0xFF;
+        int backColorRed     = (backColorRGB >>> 16) & 0xFF;
+        int backColorGreen   = (backColorRGB >>>  8) & 0xFF;
+        int backColorBlue    =  backColorRGB         & 0xFF;
 
         StringBuilder sb = new StringBuilder();
         if        (  bold &&  reverse &&  blink && !underline ) {
index bb9ae9f95af83a52c618541f121df6cbb0faecc5..908be1ef8c60400c5f3db36ef34bc62ef8b032b5 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index c24703e7a23098740804f5be8f4873b27ceb1324..b7648313d0d15513584dddf4659f42a841e8f47e 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -322,11 +322,8 @@ public class LogicalScreen implements Screen {
             // If this happens to be the cursor position, make the position
             // dirty.
             if ((cursorX == X) && (cursorY == Y)) {
-                if (physical[cursorX][cursorY].getChar() == 'Q') {
-                    physical[cursorX][cursorY].setChar('X');
-                } else {
-                    physical[cursorX][cursorY].setChar('Q');
-                }
+                physical[cursorX][cursorY].unset();
+                unsetImageRow(cursorY);
             }
         }
     }
@@ -354,7 +351,35 @@ public class LogicalScreen implements Screen {
      * @param ch character + attributes to draw
      */
     public final void putCharXY(final int x, final int y, final Cell ch) {
-        putCharXY(x, y, ch.getChar(), ch);
+        if ((x < clipLeft)
+            || (x >= clipRight)
+            || (y < clipTop)
+            || (y >= clipBottom)
+        ) {
+            return;
+        }
+
+        int X = x + offsetX;
+        int Y = y + offsetY;
+
+        // System.err.printf("putCharXY: %d, %d, %c\n", X, Y, ch);
+
+        if ((X >= 0) && (X < width) && (Y >= 0) && (Y < height)) {
+
+            // Do not put control characters on the display
+            if (!ch.isImage()) {
+                assert (ch.getChar() >= 0x20);
+                assert (ch.getChar() != 0x7F);
+            }
+            logical[X][Y].setTo(ch);
+
+            // If this happens to be the cursor position, make the position
+            // dirty.
+            if ((cursorX == X) && (cursorY == Y)) {
+                physical[cursorX][cursorY].unset();
+                unsetImageRow(cursorY);
+            }
+        }
     }
 
     /**
@@ -393,11 +418,8 @@ public class LogicalScreen implements Screen {
             // If this happens to be the cursor position, make the position
             // dirty.
             if ((cursorX == X) && (cursorY == Y)) {
-                if (physical[cursorX][cursorY].getChar() == 'Q') {
-                    physical[cursorX][cursorY].setChar('X');
-                } else {
-                    physical[cursorX][cursorY].setChar('Q');
-                }
+                physical[cursorX][cursorY].unset();
+                unsetImageRow(cursorY);
             }
         }
     }
@@ -430,11 +452,8 @@ public class LogicalScreen implements Screen {
             // If this happens to be the cursor position, make the position
             // dirty.
             if ((cursorX == X) && (cursorY == Y)) {
-                if (physical[cursorX][cursorY].getChar() == 'Q') {
-                    physical[cursorX][cursorY].setChar('X');
-                } else {
-                    physical[cursorX][cursorY].setChar('Q');
-                }
+                physical[cursorX][cursorY].unset();
+                unsetImageRow(cursorY);
             }
         }
     }
@@ -600,7 +619,7 @@ public class LogicalScreen implements Screen {
     /**
      * Draw a box with a border and empty background.
      *
-     * @param left left column of box.  0 is the left-most row.
+     * @param left left column of box.  0 is the left-most column.
      * @param top top row of the box.  0 is the top-most row.
      * @param right right column of box
      * @param bottom bottom row of the box
@@ -617,7 +636,7 @@ public class LogicalScreen implements Screen {
     /**
      * Draw a box with a border and empty background.
      *
-     * @param left left column of box.  0 is the left-most row.
+     * @param left left column of box.  0 is the left-most column.
      * @param top top row of the box.  0 is the top-most row.
      * @param right right column of box
      * @param bottom bottom row of the box
@@ -702,7 +721,7 @@ public class LogicalScreen implements Screen {
     /**
      * Draw a box shadow.
      *
-     * @param left left column of box.  0 is the left-most row.
+     * @param left left column of box.  0 is the left-most column.
      * @param top top row of the box.  0 is the top-most row.
      * @param right right column of box
      * @param bottom bottom row of the box
@@ -719,12 +738,10 @@ public class LogicalScreen implements Screen {
         // Shadows do not honor clipping but they DO honor offset.
         int oldClipRight = clipRight;
         int oldClipBottom = clipBottom;
-        /*
-        clipRight = boxWidth + 2;
-        clipBottom = boxHeight + 1;
-        */
-        clipRight = width;
-        clipBottom = height;
+        // When offsetX or offsetY go negative, we need to increase the clip
+        // bounds.
+        clipRight = width - offsetX;
+        clipBottom = height - offsetY;
 
         for (int i = 0; i < boxHeight; i++) {
             putAttrXY(boxLeft + boxWidth, boxTop + 1 + i, shadowAttr);
@@ -756,11 +773,8 @@ public class LogicalScreen implements Screen {
             && (cursorX <= width - 1)
         ) {
             // Make the current cursor position dirty
-            if (physical[cursorX][cursorY].getChar() == 'Q') {
-                physical[cursorX][cursorY].setChar('X');
-            } else {
-                physical[cursorX][cursorY].setChar('Q');
-            }
+            physical[cursorX][cursorY].unset();
+            unsetImageRow(cursorY);
         }
 
         cursorVisible = visible;
@@ -863,7 +877,21 @@ public class LogicalScreen implements Screen {
     public final void clearPhysical() {
         for (int row = 0; row < height; row++) {
             for (int col = 0; col < width; col++) {
-                physical[col][row].reset();
+                physical[col][row].unset();
+            }
+        }
+    }
+
+    /**
+     * Unset every image cell on one row of the physical screen, forcing
+     * images on that row to be redrawn.
+     *
+     * @param y row coordinate.  0 is the top-most row.
+     */
+    public final void unsetImageRow(final int y) {
+        for (int x = 0; x < width; x++) {
+            if (logical[x][y].isImage()) {
+                physical[x][y].unset();
             }
         }
     }
index 4e82d4ee694e13cbe12275ca1e16f67b42391251..5e4d3ca11224a274bdcb33a6fd5d3c338bf36ef4 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index 77688734fa9179aca6414829a5bc29cdcc91ecf7..02084100e900523033433ce205b5693fe749f2ad 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -396,7 +396,14 @@ public class MultiScreen implements Screen {
      * @return current screen height
      */
     public int getHeight() {
-        return screens.get(0).getHeight();
+        // Return the smallest height of the screens.
+        int height = screens.get(0).getHeight();
+        for (Screen screen: screens) {
+            if (screen.getHeight() < height) {
+                height = screen.getHeight();
+            }
+        }
+        return height;
     }
 
     /**
@@ -405,7 +412,14 @@ public class MultiScreen implements Screen {
      * @return current screen width
      */
     public int getWidth() {
-        return screens.get(0).getWidth();
+        // Return the smallest width of the screens.
+        int width = screens.get(0).getWidth();
+        for (Screen screen: screens) {
+            if (screen.getWidth() < width) {
+                width = screen.getWidth();
+            }
+        }
+        return width;
     }
 
     /**
@@ -496,6 +510,27 @@ public class MultiScreen implements Screen {
         }
     }
 
+    /**
+     * Clear the physical screen.
+     */
+    public void clearPhysical() {
+        for (Screen screen: screens) {
+            screen.clearPhysical();
+        }
+    }
+
+    /**
+     * Unset every image cell on one row of the physical screen, forcing
+     * images on that row to be redrawn.
+     *
+     * @param y row coordinate.  0 is the top-most row.
+     */
+    public final void unsetImageRow(final int y) {
+        for (Screen screen: screens) {
+            screen.unsetImageRow(y);
+        }
+    }
+
     /**
      * Classes must provide an implementation to push the logical screen to
      * the physical device.
@@ -590,4 +625,50 @@ public class MultiScreen implements Screen {
         }
     }
 
+    /**
+     * Get the width of a character cell in pixels.
+     *
+     * @return the width in pixels of a character cell
+     */
+    public int getTextWidth() {
+        int textWidth = 16;
+        for (Screen screen: screens) {
+            int newTextWidth = textWidth;
+            if (screen instanceof MultiScreen) {
+                newTextWidth = ((MultiScreen) screen).getTextWidth();
+            } else if (screen instanceof ECMA48Terminal) {
+                newTextWidth = ((ECMA48Terminal) screen).getTextWidth();
+            } else if (screen instanceof SwingTerminal) {
+                newTextWidth = ((SwingTerminal) screen).getTextWidth();
+            }
+            if (newTextWidth < textWidth) {
+                textWidth = newTextWidth;
+            }
+        }
+        return textWidth;
+    }
+
+    /**
+     * Get the height of a character cell in pixels.
+     *
+     * @return the height in pixels of a character cell
+     */
+    public int getTextHeight() {
+        int textHeight = 20;
+        for (Screen screen: screens) {
+            int newTextHeight = textHeight;
+            if (screen instanceof MultiScreen) {
+                newTextHeight = ((MultiScreen) screen).getTextHeight();
+            } else if (screen instanceof ECMA48Terminal) {
+                newTextHeight = ((ECMA48Terminal) screen).getTextHeight();
+            } else if (screen instanceof SwingTerminal) {
+                newTextHeight = ((SwingTerminal) screen).getTextHeight();
+            }
+            if (newTextHeight < textHeight) {
+                textHeight = newTextHeight;
+            }
+        }
+        return textHeight;
+    }
+
 }
index 0fbbea4cfc5c37f15b8e818dfb15028e2ae6ff3e..41c07562ed2c67089adb7591472899cc499de8de 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -334,6 +334,19 @@ public interface Screen {
     public void drawBoxShadow(final int left, final int top,
         final int right, final int bottom);
 
+    /**
+     * Clear the physical screen.
+     */
+    public void clearPhysical();
+
+    /**
+     * Unset every image cell on one row of the physical screen, forcing
+     * images on that row to be redrawn.
+     *
+     * @param y row coordinate.  0 is the top-most row.
+     */
+    public void unsetImageRow(final int y);
+
     /**
      * Classes must provide an implementation to push the logical screen to
      * the physical device.
index 5c2d0342eed427c36e21d62c19ded0074dddd8ad..8a29ce0b8695a13eacdfb3b1a84929c3ac925abd 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index 797caa821fbe9f14e1e8259fef86f1abc2368182..8a342b604f8d438988df943c741b91f0d54b2f06 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index 56eb8bff5419ca18f3f0117b6e98f6ae7cbebc78..92fd1d897afdb1679fd40f6a15394c09d93fdcce 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -74,13 +74,13 @@ class SwingComponent {
     /**
      * An optional border in pixels to add.
      */
-    private static final int BORDER = 5;
+    private static final int BORDER = 1;
 
     /**
      * Adjustable Insets for this component.  This has the effect of adding a
      * black border around the drawing area.
      */
-    Insets adjustInsets = new Insets(BORDER, BORDER, BORDER, BORDER);
+    Insets adjustInsets = new Insets(BORDER + 5, BORDER, BORDER, BORDER);
 
     // ------------------------------------------------------------------------
     // Constructors -----------------------------------------------------------
@@ -366,11 +366,11 @@ class SwingComponent {
         // Figure out the thickness of borders and use that to set the final
         // size.
         if (frame != null) {
-            Insets insets = frame.getInsets();
+            Insets insets = getInsets();
             frame.setSize(width + insets.left + insets.right,
                 height + insets.top + insets.bottom);
         } else {
-            Insets insets = component.getInsets();
+            Insets insets = getInsets();
             component.setSize(width + insets.left + insets.right,
                 height + insets.top + insets.bottom);
         }
index 6d1c644fd5498ad571031fbe8830a8a81985288b..28668fd346d7012000a6887e54fac459bf1d04ea 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -187,7 +187,6 @@ public class SwingSessionInfo implements SessionInfo {
             swing.getWidth(), swing.getHeight(),
             windowWidth, windowHeight);
         */
-
     }
 
     // ------------------------------------------------------------------------
index 3f5b58e3577707dd5f0474fde7f6819390909a4d..bab8f820df9ed168a8ff348235706ad6d317eb9b 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -398,14 +398,16 @@ public class SwingTerminal extends LogicalScreen
                             SwingTerminal.this.textHeight,
                             windowWidth, windowHeight);
 
-                    SwingTerminal.this.setDimensions(sessionInfo.getWindowWidth(),
-                        sessionInfo.getWindowHeight());
+                    SwingTerminal.this.setDimensions(sessionInfo.
+                        getWindowWidth(), sessionInfo.getWindowHeight());
 
                     SwingTerminal.this.resizeToScreen();
                     SwingTerminal.this.swing.setVisible(true);
                 }
             });
-        } catch (Exception e) {
+        } catch (java.lang.reflect.InvocationTargetException e) {
+            e.printStackTrace();
+        } catch (InterruptedException e) {
             e.printStackTrace();
         }
 
@@ -527,7 +529,9 @@ public class SwingTerminal extends LogicalScreen
                             SwingTerminal.this.textHeight);
                 }
             });
-        } catch (Exception e) {
+        } catch (java.lang.reflect.InvocationTargetException e) {
+            e.printStackTrace();
+        } catch (InterruptedException e) {
             e.printStackTrace();
         }
 
@@ -643,6 +647,24 @@ public class SwingTerminal extends LogicalScreen
     // SwingTerminal ----------------------------------------------------------
     // ------------------------------------------------------------------------
 
+    /**
+     * Get the width of a character cell in pixels.
+     *
+     * @return the width in pixels of a character cell
+     */
+    public int getTextWidth() {
+        return textWidth;
+    }
+
+    /**
+     * Get the height of a character cell in pixels.
+     *
+     * @return the height in pixels of a character cell
+     */
+    public int getTextHeight() {
+        return textHeight;
+    }
+
     /**
      * Setup Swing colors to match DOS color palette.
      */
@@ -727,7 +749,10 @@ public class SwingTerminal extends LogicalScreen
             Font terminus = terminusRoot.deriveFont(Font.PLAIN, fontSize);
             gotTerminus = true;
             font = terminus;
-        } catch (Exception e) {
+        } catch (java.awt.FontFormatException e) {
+            e.printStackTrace();
+            font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize);
+        } catch (java.io.IOException e) {
             e.printStackTrace();
             font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize);
         }
@@ -964,7 +989,41 @@ public class SwingTerminal extends LogicalScreen
      * Resize to font dimensions.
      */
     public void resizeToScreen() {
-        swing.setDimensions(textWidth * width, textHeight * height);
+        swing.setDimensions(textWidth * (width + 1), textHeight * (height + 1));
+    }
+
+    /**
+     * Draw one cell's image to the screen.
+     *
+     * @param gr the Swing Graphics context
+     * @param cell the Cell to draw
+     * @param xPixel the x-coordinate to render to.  0 means the
+     * left-most pixel column.
+     * @param yPixel the y-coordinate to render to.  0 means the top-most
+     * pixel row.
+     */
+    private void drawImage(final Graphics gr, final Cell cell,
+        final int xPixel, final int yPixel) {
+
+        /*
+        System.err.println("drawImage(): " + xPixel + " " + yPixel +
+            " " + cell);
+        */
+
+        // Draw the background rectangle, then the foreground character.
+        assert (cell.isImage());
+        gr.setColor(cell.getBackground());
+        gr.fillRect(xPixel, yPixel, textWidth, textHeight);
+
+        BufferedImage image = cell.getImage();
+        if (image != null) {
+            if (swing.getFrame() != null) {
+                gr.drawImage(image, xPixel, yPixel, swing.getFrame());
+            } else {
+                gr.drawImage(image, xPixel, yPixel, swing.getComponent());
+            }
+            return;
+        }
     }
 
     /**
@@ -1190,7 +1249,11 @@ public class SwingTerminal extends LogicalScreen
                         || reallyCleared
                         || (swing.getFrame() == null)) {
 
-                        drawGlyph(gr, lCell, xPixel, yPixel);
+                        if (lCell.isImage()) {
+                            drawImage(gr, lCell, xPixel, yPixel);
+                        } else {
+                            drawGlyph(gr, lCell, xPixel, yPixel);
+                        }
 
                         // Physical is always updated
                         physical[x][y].setTo(lCell);
@@ -1260,7 +1323,11 @@ public class SwingTerminal extends LogicalScreen
                                 && cursorVisible)
                             || (lCell.isBlink())
                         ) {
-                            drawGlyph(gr, lCell, xPixel, yPixel);
+                            if (lCell.isImage()) {
+                                drawImage(gr, lCell, xPixel, yPixel);
+                            } else {
+                                drawGlyph(gr, lCell, xPixel, yPixel);
+                            }
                             physical[x][y].setTo(lCell);
                         }
                     }
index 11c0faa6db735372a986dd1006d1385bdb2b89d2..ccddce4c9ecc87059b4b7999e5d32af2e85db12e 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index e6fa57afd5d1208b7b3e4c4e3dbf4aa99736cdee..d7f5bc8e246d32da560da62c7e02886e7ddf1971 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -213,7 +213,7 @@ public class TTYSessionInfo implements SessionInfo {
                     process.waitFor();
                     break;
                 } catch (InterruptedException e) {
-                    e.printStackTrace();
+                    // SQUASH
                 }
             }
             int rc = process.exitValue();
index 41809cff5c5cd0452b8d9d508de8c02fe9d3bfb9..20b9d7d63e2dd3b33c91029d38a4be77353dcf79 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index 69007321a100adb74ea987abaddabd6db6395834..8edadbf4c015ea104beded27c0134c80223ce0a3 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index dbaa6dada4b9ae58958e8d4edf707a2193e6e550..46d8ba13bf0d05b53b95cc61093002a426d9a555 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index f7b71a5f43a2b4cd8c1c1f8cc44bb54e5bae0948..d4c816f41ee88e202bf9e509af30b47630a97482 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
  */
 package jexer.bits;
 
+import java.awt.Color;
+import java.awt.image.BufferedImage;
+
 /**
- * This class represents a single text cell on the screen.
+ * This class represents a single text cell or bit of image on the screen.
  */
 public final class Cell extends CellAttributes {
 
+    // ------------------------------------------------------------------------
+    // Constants --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * The special "this cell is unset" (null) value.  This is the Unicode
+     * "not a character" value.
+     */
+    private static final char UNSET_VALUE = (char) 65535;
+
     // ------------------------------------------------------------------------
     // Variables --------------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -42,6 +55,34 @@ public final class Cell extends CellAttributes {
      */
     private char ch;
 
+    /**
+     * The image at this cell.
+     */
+    private BufferedImage image = null;
+
+    /**
+     * The image at this cell, inverted.
+     */
+    private BufferedImage invertedImage = null;
+
+    /**
+     * The background color used for the area the image portion might not
+     * cover.
+     */
+    private Color background = null;
+
+    /**
+     * hashCode() needs to call image.hashCode(), which can get quite
+     * expensive.
+     */
+    private int imageHashCode = 0;
+
+    /**
+     * hashCode() needs to call background.hashCode(), which can get quite
+     * expensive.
+     */
+    private int backgroundHashCode = 0;
+
     // ------------------------------------------------------------------------
     // Constructors -----------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -72,6 +113,98 @@ public final class Cell extends CellAttributes {
     // Cell -------------------------------------------------------------------
     // ------------------------------------------------------------------------
 
+
+    /**
+     * Set the image data for this cell.
+     *
+     * @param image the image for this cell
+     */
+    public void setImage(final BufferedImage image) {
+        this.image = image;
+        imageHashCode = image.hashCode();
+    }
+
+    /**
+     * Get the image data for this cell.
+     *
+     * @return the image for this cell
+     */
+    public BufferedImage getImage() {
+        if (invertedImage != null) {
+            return invertedImage;
+        }
+        return image;
+    }
+
+    /**
+     * Get the bitmap image background color for this cell.
+     *
+     * @return the bitmap image background color
+     */
+    public Color getBackground() {
+        return background;
+    }
+
+    /**
+     * If true, this cell has image data.
+     *
+     * @return true if this cell is an image rather than a character with
+     * attributes
+     */
+    public boolean isImage() {
+        if (image != null) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Restore the image in this cell to its normal version, if it has one.
+     */
+    public void restoreImage() {
+        invertedImage = null;
+    }
+
+    /**
+     * If true, this cell has image data, and that data is inverted.
+     *
+     * @return true if this cell is an image rather than a character with
+     * attributes, and the data is inverted
+     */
+    public boolean isInvertedImage() {
+        if ((image != null) && (invertedImage != null)) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Invert the image in this cell, if it has one.
+     */
+    public void invertImage() {
+        if (image == null) {
+            return;
+        }
+        if (invertedImage == null) {
+            invertedImage = new BufferedImage(image.getWidth(),
+                image.getHeight(), BufferedImage.TYPE_INT_ARGB);
+
+            int [] rgbArray = image.getRGB(0, 0,
+                image.getWidth(), image.getHeight(), null, 0, image.getWidth());
+
+            for (int i = 0; i < rgbArray.length; i++) {
+                // Set the colors to fully inverted.
+                if (rgbArray[i] != 0x00FFFFFF) {
+                    rgbArray[i] ^= 0x00FFFFFF;
+                }
+                // Also set alpha to non-transparent.
+                rgbArray[i] |= 0xFF000000;
+            }
+            invertedImage.setRGB(0, 0, image.getWidth(), image.getHeight(),
+                rgbArray, 0, image.getWidth());
+        }
+    }
+
     /**
      * Getter for cell character.
      *
@@ -97,6 +230,25 @@ public final class Cell extends CellAttributes {
     public void reset() {
         super.reset();
         ch = ' ';
+        image = null;
+        imageHashCode = 0;
+        invertedImage = null;
+        background = Color.BLACK;
+        backgroundHashCode = 0;
+    }
+
+    /**
+     * UNset this cell.  It will not be equal to any other cell until it has
+     * been assigned attributes and a character.
+     */
+    public void unset() {
+        super.reset();
+        ch = UNSET_VALUE;
+        image = null;
+        imageHashCode = 0;
+        invertedImage = null;
+        background = Color.BLACK;
+        backgroundHashCode = 0;
     }
 
     /**
@@ -107,6 +259,9 @@ public final class Cell extends CellAttributes {
      * @return true if this cell has default attributes.
      */
     public boolean isBlank() {
+        if ((ch == UNSET_VALUE) || (image != null)) {
+            return false;
+        }
         if ((getForeColor().equals(Color.WHITE))
             && (getBackColor().equals(Color.BLACK))
             && !isBold()
@@ -115,6 +270,7 @@ public final class Cell extends CellAttributes {
             && !isUnderline()
             && !isProtect()
             && !isRGB()
+            && !isImage()
             && (ch == ' ')
         ) {
             return true;
@@ -137,6 +293,40 @@ public final class Cell extends CellAttributes {
 
         Cell that = (Cell) rhs;
 
+        // Unsetted cells can never be equal.
+        if ((ch == UNSET_VALUE) || (that.ch == UNSET_VALUE)) {
+            return false;
+        }
+
+        // If this or rhs has an image and the other doesn't, these are not
+        // equal.
+        if ((image != null) && (that.image == null)) {
+            return false;
+        }
+        if ((image == null) && (that.image != null)) {
+            return false;
+        }
+        // If this and rhs have images, both must match.
+        if ((image != null) && (that.image != null)) {
+            if ((invertedImage == null) && (that.invertedImage != null)) {
+                return false;
+            }
+            if ((invertedImage != null) && (that.invertedImage == null)) {
+                return false;
+            }
+            // Either both objects have their image inverted, or neither do.
+            // Now if the images are identical the cells are the same
+            // visually.
+            if (image.equals(that.image)
+                && (background.equals(that.background))
+            ) {
+                return true;
+            } else {
+                return false;
+            }
+        }
+
+        // Normal case: character and attributes must match.
         if (ch == that.ch) {
             return super.equals(rhs);
         }
@@ -155,6 +345,17 @@ public final class Cell extends CellAttributes {
         int hash = A;
         hash = (B * hash) + super.hashCode();
         hash = (B * hash) + (int)ch;
+        if (image != null) {
+            /*
+            hash = (B * hash) + image.hashCode();
+            hash = (B * hash) + background.hashCode();
+             */
+            hash = (B * hash) + imageHashCode;
+            hash = (B * hash) + backgroundHashCode;
+        }
+        if (invertedImage != null) {
+            hash = (B * hash) + invertedImage.hashCode();
+        }
         return hash;
     }
 
@@ -167,11 +368,19 @@ public final class Cell extends CellAttributes {
     public void setTo(final Object rhs) {
         // Let this throw a ClassCastException
         CellAttributes thatAttr = (CellAttributes) rhs;
+        this.image = null;
+        this.imageHashCode = 0;
+        this.backgroundHashCode = 0;
         super.setTo(thatAttr);
 
         if (rhs instanceof Cell) {
             Cell that = (Cell) rhs;
             this.ch = that.ch;
+            this.image = that.image;
+            this.invertedImage = that.invertedImage;
+            this.background = that.background;
+            this.imageHashCode = that.imageHashCode;
+            this.backgroundHashCode = that.backgroundHashCode;
         }
     }
 
@@ -181,6 +390,7 @@ public final class Cell extends CellAttributes {
      * @param that a CellAttributes instance
      */
     public void setAttr(final CellAttributes that) {
+        image = null;
         super.setTo(that);
     }
 
index a4528c4401e5263dcd13f973b2d654fa67d37127..f60fd3097f856ed6e70a098c018cfccbf1a5d2b8 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index 120ff64a1c75f3dc7aeaf9da81aeb716c8787ff2..4defed5cfad7b35af25e901f2a165612b037fb71 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index 1c4670a76f3e04dd0297bd83d4915b8662149aa8..83f09f4ea41de68911696e3434b1dee5a5a08217 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -78,7 +78,7 @@ public class ColorTheme {
      * @return color associated with name, e.g. bold yellow on blue
      */
     public CellAttributes getColor(final String name) {
-        CellAttributes attr = (CellAttributes) colors.get(name);
+        CellAttributes attr = colors.get(name);
         return attr;
     }
 
@@ -153,7 +153,8 @@ public class ColorTheme {
             try {
                 foreColorRGB = Integer.parseInt(tokenizer.nextToken(), 16);
             } catch (NumberFormatException e) {
-                e.printStackTrace();
+                // Default to white on black
+                foreColorRGB = 0xFFFFFF;
             }
 
             // "on"
@@ -167,7 +168,7 @@ public class ColorTheme {
             try {
                 backColorRGB = Integer.parseInt(tokenizer.nextToken(), 16);
             } catch (NumberFormatException e) {
-                e.printStackTrace();
+                backColorRGB = 0;
             }
 
             CellAttributes color = new CellAttributes();
@@ -371,14 +372,14 @@ public class ColorTheme {
 
         // TField text
         color = new CellAttributes();
-        color.setForeColor(Color.WHITE);
-        color.setBackColor(Color.BLUE);
+        color.setForeColor(Color.BLACK);
+        color.setBackColor(Color.WHITE);
         color.setBold(false);
         colors.put("tfield.inactive", color);
         color = new CellAttributes();
-        color.setForeColor(Color.YELLOW);
-        color.setBackColor(Color.BLACK);
-        color.setBold(true);
+        color.setForeColor(Color.BLACK);
+        color.setBackColor(Color.CYAN);
+        color.setBold(false);
         colors.put("tfield.active", color);
 
         // TCheckBox
index b571639a0384d4e73fdbb8313da22e55c49f994b..58be23123906b71a4e008ad15445957432a7d198 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -145,6 +145,8 @@ 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];
+    public static final char DOWNARROWLEFT              = CP437[0xDD];
+    public static final char DOWNARROWRIGHT             = CP437[0xDE];
 
     // ------------------------------------------------------------------------
     // Constructors -----------------------------------------------------------
index f35eb50dc94d47fa8458e2bee93c27c5eebef4d2..5977ed56c71064262d19f0fed0afc15686ffd59e 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index 79b91102e0f70ce8662cf61931eb6f9430333c68..1a2079d3d209c38bccf6cf770c08661c6197180e 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index f8531dd0170e3a7fe8d15a1805b6a11ddfe36d88..cffe10e145cb8a95eafe5445165ba54c63d76241 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index d82a385e14f333142e222faadb40e8661eb6c844..97088d215fcb7c228f6a065c1cb86cfec188f9b4 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index b6572af2e9b435cd3946cc909121101c1b132f30..e6112f65da363cac3d2c515506d3ace66e92b6e5 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
  */
 package jexer.demos;
 
-import java.net.*;
-import jexer.net.*;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.text.MessageFormat;
+import java.util.ResourceBundle;
+
+import jexer.net.TelnetServerSocket;
 
 /**
  * This class is the main driver for a simple demonstration of Jexer's
@@ -38,6 +42,11 @@ import jexer.net.*;
  */
 public class Demo2 {
 
+    /**
+     * Translated strings.
+     */
+    private static final ResourceBundle i18n = ResourceBundle.getBundle(Demo2.class.getName());
+
     /**
      * Main entry point.
      *
@@ -47,7 +56,7 @@ public class Demo2 {
         ServerSocket server = null;
         try {
             if (args.length == 0) {
-                System.err.printf("USAGE: java -cp jexer.jar jexer.demos.Demo2 port\n");
+                System.err.println(i18n.getString("usageString"));
                 return;
             }
 
@@ -55,12 +64,14 @@ public class Demo2 {
             server = new TelnetServerSocket(port);
             while (true) {
                 Socket socket = server.accept();
-                System.out.printf("New connection: %s\n", socket);
+                System.out.println(MessageFormat.
+                    format(i18n.getString("newConnection"), socket));
                 DemoApplication app = new DemoApplication(socket.getInputStream(),
                     socket.getOutputStream());
-                System.out.printf("   language: %s\n",
+                System.out.println(MessageFormat.
+                    format(i18n.getString("language"),
                     ((jexer.net.TelnetInputStream) socket.getInputStream()).
-                        getLanguage());
+                        getLanguage()));
                 (new Thread(app)).start();
             }
         } catch (Exception e) {
diff --git a/src/jexer/demos/Demo2.properties b/src/jexer/demos/Demo2.properties
new file mode 100644 (file)
index 0000000..0d93747
--- /dev/null
@@ -0,0 +1,3 @@
+usageString=USAGE: java -cp jexer.jar jexer.demos.Demo2 port
+newConnection=New connection: {0}
+language=\ \ \ language: {0}
index b95f668dd1c3112c5371034f841fe18ef1e8b34f..f370f8f504a32eaade28bdccb7939949a0fb3798 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index 5de0bedfd957f72faeeb97bd958d826da259ff0e..edbc2c0024311bedb00c4fdecbdcb872b044668d 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index d383261af91c6bac99d93c5f2990d317c303de8f..e63abc1b848c37d8a9ea7b06346eefe875af7f31 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -31,6 +31,7 @@ package jexer.demos;
 import java.awt.Font;
 import java.awt.event.WindowEvent;
 import java.awt.event.WindowListener;
+import java.util.ResourceBundle;
 import javax.swing.JFrame;
 import javax.swing.JPanel;
 import javax.swing.JSplitPane;
@@ -44,6 +45,11 @@ import jexer.backend.SwingBackend;
  */
 public class Demo5 implements WindowListener {
 
+    /**
+     * Translated strings.
+     */
+    private static final ResourceBundle i18n = ResourceBundle.getBundle(Demo5.class.getName());
+
     // ------------------------------------------------------------------------
     // Variables --------------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -133,7 +139,7 @@ public class Demo5 implements WindowListener {
     // ------------------------------------------------------------------------
     // Demo5 ------------------------------------------------------------------
     // ------------------------------------------------------------------------
-    
+
     /**
      * Run two demo applications in separate panes.
      */
@@ -196,7 +202,7 @@ public class Demo5 implements WindowListener {
         mainPane.setBorder(null);
         frame.setContentPane(mainPane);
 
-        frame.setTitle("Two Jexer Apps In One Swing UI");
+        frame.setTitle(i18n.getString("frameTitle"));
         frame.setSize(1000, 640);
         frame.setVisible(true);
     }
diff --git a/src/jexer/demos/Demo5.properties b/src/jexer/demos/Demo5.properties
new file mode 100644 (file)
index 0000000..56b419d
--- /dev/null
@@ -0,0 +1 @@
+frameTitle=Two Jexer Apps In One Swing UI
index 9f949131caf5f764f0e5c472cd56c648cb2f07ca..236e7a276733410d95d80224be04ccb792adc889 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -30,6 +30,7 @@ package jexer.demos;
 
 import jexer.TApplication;
 import jexer.backend.*;
+import jexer.demos.DemoApplication;
 
 /**
  * This class shows off the use of MultiBackend and MultiScreen.
index a9b3468bbfa10288ab98b59e72c43ff18c7e8c75..18d47f37e5a348002ecadd2df9e2755d7b920769 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
  */
 package jexer.demos;
 
-import java.io.*;
-
-import jexer.*;
-import jexer.event.*;
-import jexer.menu.*;
+import java.io.File;
+import java.io.InputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.io.Reader;
+import java.io.UnsupportedEncodingException;
+import java.util.ResourceBundle;
+
+import jexer.TApplication;
+import jexer.TEditColorThemeWindow;
+import jexer.TEditorWindow;
+import jexer.event.TMenuEvent;
+import jexer.menu.TMenu;
+import jexer.menu.TMenuItem;
+import jexer.menu.TSubMenu;
 import jexer.backend.Backend;
 import jexer.backend.SwingTerminal;
 
@@ -41,6 +52,11 @@ import jexer.backend.SwingTerminal;
  */
 public class DemoApplication extends TApplication {
 
+    /**
+     * Translated strings.
+     */
+    private static ResourceBundle i18n = ResourceBundle.getBundle(DemoApplication.class.getName());
+
     // ------------------------------------------------------------------------
     // Constructors -----------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -63,7 +79,7 @@ public class DemoApplication extends TApplication {
         super(input, output);
         addAllWidgets();
 
-        getBackend().setTitle("Jexer Demo Application");
+        getBackend().setTitle(i18n.getString("applicationTitle"));
     }
 
     /**
@@ -83,7 +99,7 @@ public class DemoApplication extends TApplication {
         super(input, reader, writer, setRawMode);
         addAllWidgets();
 
-        getBackend().setTitle("Jexer Demo Application");
+        getBackend().setTitle(i18n.getString("applicationTitle"));
     }
 
     /**
@@ -121,7 +137,7 @@ public class DemoApplication extends TApplication {
     public DemoApplication(final BackendType backendType) throws Exception {
         super(backendType);
         addAllWidgets();
-        getBackend().setTitle("Jexer Demo Application");
+        getBackend().setTitle(i18n.getString("applicationTitle"));
     }
 
     // ------------------------------------------------------------------------
@@ -190,33 +206,33 @@ public class DemoApplication extends TApplication {
         addFileMenu();
         addEditMenu();
 
-        TMenu demoMenu = addMenu("&Demo");
-        TMenuItem item = demoMenu.addItem(2000, "&Checkable");
+        TMenu demoMenu = addMenu(i18n.getString("demo"));
+        TMenuItem item = demoMenu.addItem(2000, i18n.getString("checkable"));
         item.setCheckable(true);
-        item = demoMenu.addItem(2001, "Disabled");
+        item = demoMenu.addItem(2001, i18n.getString("disabled"));
         item.setEnabled(false);
-        item = demoMenu.addItem(2002, "&Normal");
-        TSubMenu subMenu = demoMenu.addSubMenu("Sub-&Menu");
-        item = demoMenu.addItem(2010, "N&ormal A&&D");
-        item = demoMenu.addItem(2050, "Co&lors...");
+        item = demoMenu.addItem(2002, i18n.getString("normal"));
+        TSubMenu subMenu = demoMenu.addSubMenu(i18n.getString("subMenu"));
+        item = demoMenu.addItem(2010, i18n.getString("normal"));
+        item = demoMenu.addItem(2050, i18n.getString("colors"));
 
-        item = subMenu.addItem(2000, "&Checkable (sub)");
+        item = subMenu.addItem(2000, i18n.getString("checkableSub"));
         item.setCheckable(true);
-        item = subMenu.addItem(2001, "Disabled (sub)");
+        item = subMenu.addItem(2001, i18n.getString("disabledSub"));
         item.setEnabled(false);
-        item = subMenu.addItem(2002, "&Normal (sub)");
+        item = subMenu.addItem(2002, i18n.getString("normalSub"));
 
-        subMenu = subMenu.addSubMenu("Sub-&Menu");
-        item = subMenu.addItem(2000, "&Checkable (sub)");
+        subMenu = subMenu.addSubMenu(i18n.getString("subMenu"));
+        item = subMenu.addItem(2000, i18n.getString("checkableSub"));
         item.setCheckable(true);
-        item = subMenu.addItem(2001, "Disabled (sub)");
+        item = subMenu.addItem(2001, i18n.getString("disabledSub"));
         item.setEnabled(false);
-        item = subMenu.addItem(2002, "&Normal (sub)");
+        item = subMenu.addItem(2002, i18n.getString("normalSub"));
 
         if (getScreen() instanceof SwingTerminal) {
-            TMenu swingMenu = addMenu("Swin&g");
-            item = swingMenu.addItem(3000, "&Bigger +2");
-            item = swingMenu.addItem(3001, "&Smaller -2");
+            TMenu swingMenu = addMenu(i18n.getString("swing"));
+            item = swingMenu.addItem(3000, i18n.getString("bigger"));
+            item = swingMenu.addItem(3001, i18n.getString("smaller"));
         }
 
         addWindowMenu();
diff --git a/src/jexer/demos/DemoApplication.properties b/src/jexer/demos/DemoApplication.properties
new file mode 100644 (file)
index 0000000..95d8603
--- /dev/null
@@ -0,0 +1,15 @@
+applicationTitle=Demo Application
+
+demo=&Demo
+checkable=&Checkable
+disabled=Disabled
+normal=&Normal
+subMenu=Sub-&Menu
+normal=N&ormal A&&D
+colors=Co&lors...
+checkableSub=&Checkable (sub)
+disabledSub=Disabled (sub)
+normalSub=&Normal (sub)
+swing=Swin&g
+bigger=&Bigger +2
+smaller=&Smaller -2
index 46680519fc96c3b9571d873403902235d5674c81..c982244f9d2e5ad1bf68ef36ce552a6262169dff 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
  */
 package jexer.demos;
 
+import java.text.MessageFormat;
 import java.util.ArrayList;
 import java.util.List;
-
-import jexer.*;
+import java.util.ResourceBundle;
+
+import jexer.TAction;
+import jexer.TApplication;
+import jexer.TComboBox;
+import jexer.TMessageBox;
+import jexer.TRadioGroup;
+import jexer.TWindow;
 import static jexer.TCommand.*;
 import static jexer.TKeypress.*;
 
@@ -41,6 +48,11 @@ import static jexer.TKeypress.*;
  */
 public class DemoCheckBoxWindow extends TWindow {
 
+    /**
+     * Translated strings.
+     */
+    private static final ResourceBundle i18n = ResourceBundle.getBundle(DemoCheckBoxWindow.class.getName());
+
     // ------------------------------------------------------------------------
     // Variables --------------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -73,50 +85,49 @@ public class DemoCheckBoxWindow extends TWindow {
     DemoCheckBoxWindow(final TApplication parent, final int flags) {
         // Construct a demo window.  X and Y don't matter because it will be
         // centered on screen.
-        super(parent, "Radiobuttons, CheckBoxes, and ComboBox",
-            0, 0, 60, 17, flags);
+        super(parent, i18n.getString("windowTitle"), 0, 0, 60, 17, flags);
 
         int row = 1;
 
         // Add some widgets
-        addLabel("Check box example 1", 1, row);
-        addCheckBox(35, row++, "CheckBox 1", false);
-        addLabel("Check box example 2", 1, row);
-        addCheckBox(35, row++, "CheckBox 2", true);
+        addLabel(i18n.getString("checkBoxLabel1"), 1, row);
+        addCheckBox(35, row++, i18n.getString("checkBoxText1"), false);
+        addLabel(i18n.getString("checkBoxLabel2"), 1, row);
+        addCheckBox(35, row++, i18n.getString("checkBoxText2"), true);
         row += 2;
 
-        TRadioGroup group = addRadioGroup(1, row, "Group 1");
-        group.addRadioButton("Radio option 1");
-        group.addRadioButton("Radio option 2");
-        group.addRadioButton("Radio option 3");
+        TRadioGroup group = addRadioGroup(1, row,
+            i18n.getString("radioGroupTitle"));
+        group.addRadioButton(i18n.getString("radioOption1"));
+        group.addRadioButton(i18n.getString("radioOption2"));
+        group.addRadioButton(i18n.getString("radioOption3"));
 
         List<String> comboValues = new ArrayList<String>();
-        comboValues.add("String 0");
-        comboValues.add("String 1");
-        comboValues.add("String 2");
-        comboValues.add("String 3");
-        comboValues.add("String 4");
-        comboValues.add("String 5");
-        comboValues.add("String 6");
-        comboValues.add("String 7");
-        comboValues.add("String 8");
-        comboValues.add("String 9");
-        comboValues.add("String 10");
+        comboValues.add(i18n.getString("comboBoxString0"));
+        comboValues.add(i18n.getString("comboBoxString1"));
+        comboValues.add(i18n.getString("comboBoxString2"));
+        comboValues.add(i18n.getString("comboBoxString3"));
+        comboValues.add(i18n.getString("comboBoxString4"));
+        comboValues.add(i18n.getString("comboBoxString5"));
+        comboValues.add(i18n.getString("comboBoxString6"));
+        comboValues.add(i18n.getString("comboBoxString7"));
+        comboValues.add(i18n.getString("comboBoxString8"));
+        comboValues.add(i18n.getString("comboBoxString9"));
+        comboValues.add(i18n.getString("comboBoxString10"));
 
         comboBox = addComboBox(35, row, 12, comboValues, 2, 6,
             new TAction() {
                 public void DO() {
-                    getApplication().messageBox("ComboBox",
-                        "You selected the following value:\n" +
-                        "\n" +
-                        comboBox.getText() +
-                        "\n",
+                    getApplication().messageBox(i18n.getString("messageBoxTitle"),
+                        MessageFormat.format(i18n.getString("messageBoxPrompt"),
+                            comboBox.getText()),
                         TMessageBox.Type.OK);
                 }
             }
         );
 
-        addButton("&Close Window", (getWidth() - 14) / 2, getHeight() - 4,
+        addButton(i18n.getString("closeWindow"),
+            (getWidth() - 14) / 2, getHeight() - 4,
             new TAction() {
                 public void DO() {
                     DemoCheckBoxWindow.this.getApplication()
@@ -125,11 +136,15 @@ public class DemoCheckBoxWindow extends TWindow {
             }
         );
 
-        statusBar = newStatusBar("Radiobuttons and checkboxes");
-        statusBar.addShortcutKeypress(kbF1, cmHelp, "Help");
-        statusBar.addShortcutKeypress(kbF2, cmShell, "Shell");
-        statusBar.addShortcutKeypress(kbF3, cmOpen, "Open");
-        statusBar.addShortcutKeypress(kbF10, cmExit, "Exit");
+        statusBar = newStatusBar(i18n.getString("statusBar"));
+        statusBar.addShortcutKeypress(kbF1, cmHelp,
+            i18n.getString("statusBarHelp"));
+        statusBar.addShortcutKeypress(kbF2, cmShell,
+            i18n.getString("statusBarShell"));
+        statusBar.addShortcutKeypress(kbF3, cmOpen,
+            i18n.getString("statusBarOpen"));
+        statusBar.addShortcutKeypress(kbF10, cmExit,
+            i18n.getString("statusBarExit"));
     }
 
 }
diff --git a/src/jexer/demos/DemoCheckBoxWindow.properties b/src/jexer/demos/DemoCheckBoxWindow.properties
new file mode 100644 (file)
index 0000000..61210ce
--- /dev/null
@@ -0,0 +1,30 @@
+windowTitle=Radiobuttons, CheckBoxes, and ComboBox
+
+statusBar=Radiobuttons and checkboxes
+statusBarHelp=Help
+statusBarShell=Shell
+statusBarOpen=Open
+statusBarExit=Exit
+
+checkBoxLabel1=Check box example 1
+checkBoxText1=CheckBox 1
+checkBoxLabel2=Check box example 2
+checkBoxText2=CheckBox 2
+radioGroupTitle=Group 1
+radioOption1=Radio option 1
+radioOption2=Radio option 2
+radioOption3=Radio option 3
+comboBoxString0=String 0
+comboBoxString1=String 1
+comboBoxString2=String 2
+comboBoxString3=String 3
+comboBoxString4=String 4
+comboBoxString5=String 5
+comboBoxString6=String 6
+comboBoxString7=String 7
+comboBoxString8=String 8
+comboBoxString9=String 9
+comboBoxString10=String 10
+messageBoxTitle=ComboBox
+messageBoxPrompt=You selected the following value:\n\n{0}\n
+closeWindow=&Close Window
index 104715997261206c0d54b1cadd4bf54e70360751..c63ce0d353ca0eea49661d93ac53ee776ca10848 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
  */
 package jexer.demos;
 
-import jexer.*;
-import jexer.event.*;
+import java.util.ResourceBundle;
+
+import jexer.TApplication;
+import jexer.TEditorWidget;
+import jexer.TWidget;
+import jexer.TWindow;
+import jexer.event.TResizeEvent;
 import static jexer.TCommand.*;
 import static jexer.TKeypress.*;
 
@@ -38,6 +43,11 @@ import static jexer.TKeypress.*;
  */
 public class DemoEditorWindow extends TWindow {
 
+    /**
+     * Translated strings.
+     */
+    private static final ResourceBundle i18n = ResourceBundle.getBundle(DemoEditorWindow.class.getName());
+
     // ------------------------------------------------------------------------
     // Variables --------------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -62,12 +72,15 @@ public class DemoEditorWindow extends TWindow {
         final String text) {
 
         super(parent, title, 0, 0, 44, 22, RESIZABLE);
-        editField = addEditor(text, 0, 0, 42, 20);
+        editField = new TEditorWidget(this, text, 0, 0, 42, 20);
 
-        statusBar = newStatusBar("Editable text demo window");
-        statusBar.addShortcutKeypress(kbF1, cmHelp, "Help");
-        statusBar.addShortcutKeypress(kbF2, cmShell, "Shell");
-        statusBar.addShortcutKeypress(kbF10, cmExit, "Exit");
+        statusBar = newStatusBar(i18n.getString("statusBar"));
+        statusBar.addShortcutKeypress(kbF1, cmHelp,
+            i18n.getString("statusBarHelp"));
+        statusBar.addShortcutKeypress(kbF2, cmShell,
+            i18n.getString("statusBarShell"));
+        statusBar.addShortcutKeypress(kbF10, cmExit,
+            i18n.getString("statusBarExit"));
     }
 
     /**
@@ -76,7 +89,7 @@ public class DemoEditorWindow extends TWindow {
      * @param parent the main application
      */
     public DemoEditorWindow(final TApplication parent) {
-        this(parent, "Editor",
+        this(parent, i18n.getString("windowTitle"),
 "This is an example of an editable text field.  Some example text follows.\n" +
 "\n" +
 "This library implements a text-based windowing system loosely\n" +
diff --git a/src/jexer/demos/DemoEditorWindow.properties b/src/jexer/demos/DemoEditorWindow.properties
new file mode 100644 (file)
index 0000000..3fa3212
--- /dev/null
@@ -0,0 +1,6 @@
+windowTitle=Editor
+
+statusBar=Editable text demo window
+statusBarHelp=Help
+statusBarShell=Shell
+statusBarExit=Exit
index f1cbe2122cdbb54ee2088a3fbed979d805cecfdd..1ae35f0b7dab058f42fdef8e7b6946241d00b6fe 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
  */
 package jexer.demos;
 
-import java.io.*;
-import java.util.*;
-
-import jexer.*;
-import jexer.event.*;
+import java.io.File;
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.Locale;
+import java.util.ResourceBundle;
+
+import jexer.TAction;
+import jexer.TApplication;
+import jexer.TEditColorThemeWindow;
+import jexer.TEditorWindow;
+import jexer.TLabel;
+import jexer.TProgressBar;
+import jexer.TTimer;
+import jexer.TWidget;
+import jexer.TWindow;
+import jexer.event.TCommandEvent;
 import static jexer.TCommand.*;
 import static jexer.TKeypress.*;
 
@@ -42,6 +55,11 @@ import static jexer.TKeypress.*;
  */
 public class DemoMainWindow extends TWindow {
 
+    /**
+     * Translated strings.
+     */
+    private static final ResourceBundle i18n = ResourceBundle.getBundle(DemoMainWindow.class.getName());
+
     // ------------------------------------------------------------------------
     // Variables --------------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -101,13 +119,13 @@ public class DemoMainWindow extends TWindow {
     private DemoMainWindow(final TApplication parent, final int flags) {
         // Construct a demo window.  X and Y don't matter because it will be
         // centered on screen.
-        super(parent, "Demo Window", 0, 0, 64, 23, flags);
+        super(parent, i18n.getString("windowTitle"), 0, 0, 64, 23, flags);
 
         int row = 1;
 
         // Add some widgets
-        addLabel("Message Boxes", 1, row);
-        TWidget first = addButton("&MessageBoxes", 35, row,
+        addLabel(i18n.getString("messageBoxLabel"), 1, row);
+        TWidget first = addButton(i18n.getString("messageBoxButton"), 35, row,
             new TAction() {
                 public void DO() {
                     new DemoMsgBoxWindow(getApplication());
@@ -116,8 +134,8 @@ public class DemoMainWindow extends TWindow {
         );
         row += 2;
 
-        addLabel("Open me as modal", 1, row);
-        addButton("W&indow", 35, row,
+        addLabel(i18n.getString("openModalLabel"), 1, row);
+        addButton(i18n.getString("openModalButton"), 35, row,
             new TAction() {
                 public void DO() {
                     new DemoMainWindow(getApplication(), MODAL);
@@ -126,8 +144,8 @@ public class DemoMainWindow extends TWindow {
         );
         row += 2;
 
-        addLabel("Text fields and calendar", 1, row);
-        addButton("Field&s", 35, row,
+        addLabel(i18n.getString("textFieldLabel"), 1, row);
+        addButton(i18n.getString("textFieldButton"), 35, row,
             new TAction() {
                 public void DO() {
                     new DemoTextFieldWindow(getApplication());
@@ -136,8 +154,8 @@ public class DemoMainWindow extends TWindow {
         );
         row += 2;
 
-        addLabel("Radio buttons, check and combobox", 1, row);
-        addButton("&CheckBoxes", 35, row,
+        addLabel(i18n.getString("radioButtonLabel"), 1, row);
+        addButton(i18n.getString("radioButtonButton"), 35, row,
             new TAction() {
                 public void DO() {
                     new DemoCheckBoxWindow(getApplication());
@@ -146,15 +164,15 @@ public class DemoMainWindow extends TWindow {
         );
         row += 2;
 
-        addLabel("Editor window", 1, row);
-        addButton("&1 Widget", 35, row,
+        addLabel(i18n.getString("editorLabel"), 1, row);
+        addButton(i18n.getString("editorButton1"), 35, row,
             new TAction() {
                 public void DO() {
                     new DemoEditorWindow(getApplication());
                 }
             }
         );
-        addButton("&2 Window", 48, row,
+        addButton(i18n.getString("editorButton2"), 48, row,
             new TAction() {
                 public void DO() {
                     new TEditorWindow(getApplication());
@@ -163,8 +181,8 @@ public class DemoMainWindow extends TWindow {
         );
         row += 2;
 
-        addLabel("Text areas", 1, row);
-        addButton("&Text", 35, row,
+        addLabel(i18n.getString("textAreaLabel"), 1, row);
+        addButton(i18n.getString("textAreaButton"), 35, row,
             new TAction() {
                 public void DO() {
                     new DemoTextWindow(getApplication());
@@ -173,8 +191,8 @@ public class DemoMainWindow extends TWindow {
         );
         row += 2;
 
-        addLabel("Tree views", 1, row);
-        addButton("Tree&View", 35, row,
+        addLabel(i18n.getString("treeViewLabel"), 1, row);
+        addButton(i18n.getString("treeViewButton"), 35, row,
             new TAction() {
                 public void DO() {
                     try {
@@ -187,8 +205,8 @@ public class DemoMainWindow extends TWindow {
         );
         row += 2;
 
-        addLabel("Terminal", 1, row);
-        addButton("Termi&nal", 35, row,
+        addLabel(i18n.getString("terminalLabel"), 1, row);
+        addButton(i18n.getString("terminalButton"), 35, row,
             new TAction() {
                 public void DO() {
                     getApplication().openTerminal(0, 0);
@@ -197,8 +215,8 @@ public class DemoMainWindow extends TWindow {
         );
         row += 2;
 
-        addLabel("Color editor", 1, row);
-        addButton("Co&lors", 35, row,
+        addLabel(i18n.getString("colorEditorLabel"), 1, row);
+        addButton(i18n.getString("colorEditorButton"), 35, row,
             new TAction() {
                 public void DO() {
                     new TEditColorThemeWindow(getApplication());
@@ -209,12 +227,13 @@ public class DemoMainWindow extends TWindow {
 
         progressBar = addProgressBar(1, row, 22, 0);
         row++;
-        timerLabel = addLabel("Timer", 1, row);
+        timerLabel = addLabel(i18n.getString("timerLabel"), 1, row);
         timer = getApplication().addTimer(250, true,
             new TAction() {
 
                 public void DO() {
-                    timerLabel.setLabel(String.format("Timer: %d", timerI));
+                    timerLabel.setLabel(String.format(i18n.
+                            getString("timerText"), timerI));
                     timerLabel.setWidth(timerLabel.getLabel().length());
                     if (timerI < 100) {
                         timerI++;
@@ -255,11 +274,15 @@ public class DemoMainWindow extends TWindow {
 
         activate(first);
 
-        statusBar = newStatusBar("Demo Main Window");
-        statusBar.addShortcutKeypress(kbF1, cmHelp, "Help");
-        statusBar.addShortcutKeypress(kbF2, cmShell, "Shell");
-        statusBar.addShortcutKeypress(kbF3, cmOpen, "Open");
-        statusBar.addShortcutKeypress(kbF10, cmExit, "Exit");
+        statusBar = newStatusBar(i18n.getString("statusBar"));
+        statusBar.addShortcutKeypress(kbF1, cmHelp,
+            i18n.getString("statusBarHelp"));
+        statusBar.addShortcutKeypress(kbF2, cmShell,
+            i18n.getString("statusBarShell"));
+        statusBar.addShortcutKeypress(kbF3, cmOpen,
+            i18n.getString("statusBarOpen"));
+        statusBar.addShortcutKeypress(kbF10, cmExit,
+            i18n.getString("statusBarExit"));
     }
 
     // ------------------------------------------------------------------------
@@ -288,15 +311,18 @@ public class DemoMainWindow extends TWindow {
                 String filename = fileOpenBox(".");
                 if (filename != null) {
                     try {
-                        new TEditorWindow(getApplication(), new File(filename));
+                        new TEditorWindow(getApplication(),
+                            new File(filename));
                     } catch (IOException e) {
-                        messageBox("Error", "Error reading file: " +
-                            e.getMessage());
+                        messageBox(i18n.getString("errorTitle"),
+                            MessageFormat.format(i18n.
+                                getString("errorReadingFile"), e.getMessage()));
                     }
                 }
             } catch (IOException e) {
-                messageBox("Error", "Error opening file dialog: " +
-                    e.getMessage());
+                        messageBox(i18n.getString("errorTitle"),
+                            MessageFormat.format(i18n.
+                                getString("errorOpeningFile"), e.getMessage()));
             }
             return;
         }
diff --git a/src/jexer/demos/DemoMainWindow.properties b/src/jexer/demos/DemoMainWindow.properties
new file mode 100644 (file)
index 0000000..4a512cb
--- /dev/null
@@ -0,0 +1,33 @@
+windowTitle=Demo Window
+
+statusBar=Demo Main Window
+statusBarHelp=Help
+statusBarShell=Shell
+statusBarOpen=Open
+statusBarExit=Exit
+
+messageBoxLabel=Message Boxes
+messageBoxButton=&MessageBoxes
+openModalLabel=Open me as modal
+openModalButton=W&indow
+textFieldLabel=Text fields and calendar
+textFieldButton=Field&s
+radioButtonLabel=Radio buttons, check and combobox
+radioButtonButton=&CheckBoxes
+editorLabel=Editor window
+editorButton1=&1 Widget
+editorButton2=&2 Window
+textAreaLabel=Text areas
+textAreaButton=&Text
+treeViewLabel=Tree views
+treeViewButton=Tree&View
+terminalLabel=Terminal
+terminalButton=Termi&nal
+colorEditorLabel=Color editor
+colorEditorButton=Co&lors
+timerLabel=Timer
+timerText=Timer: %d
+
+errorTitle=Error
+errorReadingFile=Error reading file: {0}
+errorOpeningFile=Error opening file dialog: {0}
index ee2bca1ef35993df63dfe9255461cbf0ca1ae8c4..c2d4ff61e8d5baf49cb59216ed9c96af222681b4 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
  */
 package jexer.demos;
 
-import jexer.*;
+import java.text.MessageFormat;
+import java.util.ResourceBundle;
+
+import jexer.TAction;
+import jexer.TApplication;
+import jexer.TInputBox;
+import jexer.TMessageBox;
+import jexer.TWindow;
 import static jexer.TCommand.*;
 import static jexer.TKeypress.*;
 
@@ -37,6 +44,11 @@ import static jexer.TKeypress.*;
  */
 public class DemoMsgBoxWindow extends TWindow {
 
+    /**
+     * Translated strings.
+     */
+    private static final ResourceBundle i18n = ResourceBundle.getBundle(DemoMsgBoxWindow.class.getName());
+
     // ------------------------------------------------------------------------
     // Constructors -----------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -59,120 +71,101 @@ public class DemoMsgBoxWindow extends TWindow {
     DemoMsgBoxWindow(final TApplication parent, final int flags) {
         // Construct a demo window.  X and Y don't matter because it
         // will be centered on screen.
-        super(parent, "Message Boxes", 0, 0, 64, 18, flags);
+        super(parent, i18n.getString("windowTitle"), 0, 0, 64, 18, flags);
 
         int row = 1;
 
         // Add some widgets
-        addLabel("Default OK message box", 1, row);
-        addButton("Open O&K MB", 35, row,
+        addLabel(i18n.getString("messageBoxLabel1"), 1, row);
+        addButton(i18n.getString("messageBoxButton1"), 35, row,
             new TAction() {
                 public void DO() {
-                    getApplication().messageBox("OK MessageBox",
-"This is an example of a OK MessageBox.  This is the\n" +
-"default MessageBox.\n" +
-"\n" +
-"Note that the MessageBox text can span multiple\n" +
-"lines.\n" +
-"\n" +
-"The default result (if someone hits the top-left\n" +
-"close button) is OK.\n",
+                    getApplication().messageBox(i18n.
+                        getString("messageBoxTitle1"),
+                        i18n.getString("messageBoxPrompt1"),
                         TMessageBox.Type.OK);
                 }
             }
         );
         row += 2;
 
-        addLabel("OK/Cancel message box", 1, row);
-        addButton("O&pen OKC MB", 35, row,
+        addLabel(i18n.getString("messageBoxLabel2"), 1, row);
+        addButton(i18n.getString("messageBoxButton2"), 35, row,
             new TAction() {
                 public void DO() {
-                    getApplication().messageBox("OK/Cancel MessageBox",
-"This is an example of a OK/Cancel MessageBox.\n" +
-"\n" +
-"Note that the MessageBox text can span multiple\n" +
-"lines.\n" +
-"\n" +
-"The default result (if someone hits the top-left\n" +
-"close button) is CANCEL.\n",
+                    getApplication().messageBox(i18n.
+                        getString("messageBoxTitle2"),
+                        i18n.getString("messageBoxPrompt2"),
                         TMessageBox.Type.OKCANCEL);
                 }
             }
         );
         row += 2;
 
-        addLabel("Yes/No message box", 1, row);
-        addButton("Open &YN MB", 35, row,
+        addLabel(i18n.getString("messageBoxLabel3"), 1, row);
+        addButton(i18n.getString("messageBoxButton3"), 35, row,
             new TAction() {
                 public void DO() {
-                    getApplication().messageBox("Yes/No MessageBox",
-"This is an example of a Yes/No MessageBox.\n" +
-"\n" +
-"Note that the MessageBox text can span multiple\n" +
-"lines.\n" +
-"\n" +
-"The default result (if someone hits the top-left\n" +
-"close button) is NO.\n",
+                    getApplication().messageBox(i18n.
+                        getString("messageBoxTitle3"),
+                        i18n.getString("messageBoxPrompt3"),
                         TMessageBox.Type.YESNO);
                 }
             }
         );
         row += 2;
 
-        addLabel("Yes/No/Cancel message box", 1, row);
-        addButton("Ope&n YNC MB", 35, row,
+        addLabel(i18n.getString("messageBoxLabel4"), 1, row);
+        addButton(i18n.getString("messageBoxButton4"), 35, row,
             new TAction() {
                 public void DO() {
-                    getApplication().messageBox("Yes/No/Cancel MessageBox",
-"This is an example of a Yes/No/Cancel MessageBox.\n" +
-"\n" +
-"Note that the MessageBox text can span multiple\n" +
-"lines.\n" +
-"\n" +
-"The default result (if someone hits the top-left\n" +
-"close button) is CANCEL.\n",
+                    getApplication().messageBox(i18n.
+                        getString("messageBoxTitle4"),
+                        i18n.getString("messageBoxPrompt4"),
                         TMessageBox.Type.YESNOCANCEL);
                 }
             }
         );
         row += 2;
 
-        addLabel("Input box 1", 1, row);
-        addButton("Open &input box", 35, row,
+        addLabel(i18n.getString("inputBoxLabel1"), 1, row);
+        addButton(i18n.getString("inputBoxButton1"), 35, row,
             new TAction() {
                 public void DO() {
-                    TInputBox in = getApplication().inputBox("Input Box",
-"This is an example of an InputBox.\n" +
-"\n" +
-"Note that the InputBox text can span multiple\n" +
-"lines.\n",
-                        "some input text");
-                    getApplication().messageBox("Your InputBox Answer",
-                        "You entered: " + in.getText());
+                    TInputBox in = getApplication().inputBox(i18n.
+                        getString("inputBoxTitle1"),
+                        i18n.getString("inputBoxPrompt1"),
+                        i18n.getString("inputBoxInput1"));
+                    getApplication().messageBox(i18n.
+                        getString("inputBoxAnswerTitle1"),
+                        MessageFormat.format(i18n.
+                            getString("inputBoxAnswerPrompt1"), in.getText()));
                 }
             }
         );
         row += 2;
 
-        addLabel("Input box 2", 1, row);
-        addButton("Cance&llable input box", 35, row,
+        addLabel(i18n.getString("inputBoxLabel2"), 1, row);
+        addButton(i18n.getString("inputBoxButton2"), 35, row,
             new TAction() {
                 public void DO() {
-                    TInputBox in = getApplication().inputBox("Input Box",
-"This is an example of an InputBox.\n" +
-"\n" +
-"Note that the InputBox text can span multiple\n" +
-"lines.\n" +
-"This one has both OK and Cancel buttons.\n",
-                        "some input text", TInputBox.Type.OKCANCEL);
-                    getApplication().messageBox("Your InputBox Answer",
-                        "You entered: " + in.getText() + " and pressed " +
-                        in.getResult());
+                    TInputBox in = getApplication().inputBox(i18n.
+                        getString("inputBoxTitle2"),
+                        i18n.getString("inputBoxPrompt2"),
+                        i18n.getString("inputBoxInput2"),
+                        TInputBox.Type.OKCANCEL);
+                    getApplication().messageBox(i18n.
+                        getString("inputBoxAnswerTitle2"),
+                        MessageFormat.format(i18n.
+                            getString("inputBoxAnswerPrompt2"), in.getText(),
+                            in.getResult()));
                 }
             }
         );
+        row += 2;
 
-        addButton("&Close Window", (getWidth() - 14) / 2, getHeight() - 4,
+        addButton(i18n.getString("closeWindow"),
+            (getWidth() - 14) / 2, getHeight() - 4,
             new TAction() {
                 public void DO() {
                     getApplication().closeWindow(DemoMsgBoxWindow.this);
@@ -180,10 +173,14 @@ public class DemoMsgBoxWindow extends TWindow {
             }
         );
 
-        statusBar = newStatusBar("Message boxes");
-        statusBar.addShortcutKeypress(kbF1, cmHelp, "Help");
-        statusBar.addShortcutKeypress(kbF2, cmShell, "Shell");
-        statusBar.addShortcutKeypress(kbF3, cmOpen, "Open");
-        statusBar.addShortcutKeypress(kbF10, cmExit, "Exit");
+        statusBar = newStatusBar(i18n.getString("statusBar"));
+        statusBar.addShortcutKeypress(kbF1, cmHelp,
+            i18n.getString("statusBarHelp"));
+        statusBar.addShortcutKeypress(kbF2, cmShell,
+            i18n.getString("statusBarShell"));
+        statusBar.addShortcutKeypress(kbF3, cmOpen,
+            i18n.getString("statusBarOpen"));
+        statusBar.addShortcutKeypress(kbF10, cmExit,
+            i18n.getString("statusBarExit"));
     }
 }
diff --git a/src/jexer/demos/DemoMsgBoxWindow.properties b/src/jexer/demos/DemoMsgBoxWindow.properties
new file mode 100644 (file)
index 0000000..47a858a
--- /dev/null
@@ -0,0 +1,45 @@
+windowTitle=Message Boxes
+
+statusBar=Message boxes
+statusBarHelp=Help
+statusBarShell=Shell
+statusBarOpen=Open
+statusBarExit=Exit
+
+messageBoxLabel1=Default OK message box
+messageBoxButton1=Open O&K MB
+messageBoxTitle1=OK MessageBox
+messageBoxPrompt1=This is an example of a OK MessageBox.  This is the\ndefault MessageBox.\n\nNote that the MessageBox text can span multiple\nlines.\n\nThe default result (if someone hits the top-left\nclose button) is OK.\n
+
+messageBoxLabel2=OK/Cancel message box
+messageBoxButton2=O&pen OKC MB
+messageBoxTitle2=OK/Cancel MessageBox
+messageBoxPrompt2=This is an example of a OK/Cancel MessageBox.\n\nNote that the MessageBox text can span multiple\nlines.\n\nThe default result (if someone hits the top-leftclose button) is CANCEL.\n
+
+messageBoxLabel3=Yes/No message box
+messageBoxButton3=Open &YN MB
+messageBoxTitle3=Yes/No MessageBox
+messageBoxPrompt3=This is an example of a Yes/No MessageBox.\n\nNote that the MessageBox text can span multiple\nlines.\n\nThe default result (if someone hits the top-left\nclose button) is NO.\n
+
+messageBoxLabel4=Yes/No/Cancel message box
+messageBoxButton4=Ope&n YNC MB
+messageBoxTitle4=Yes/No/Cancel MessageBox
+messageBoxPrompt4=This is an example of a Yes/No/Cancel MessageBox.\n\nNote that the MessageBox text can span multiple\nlines.\n\nThe default result (if someone hits the top-left\nclose button) is CANCEL.\n
+
+inputBoxLabel1=Input box 1
+inputBoxButton1=Open &input box
+inputBoxTitle1=Input Box
+inputBoxPrompt1=This is an example of an InputBox.\n\nNote that the InputBox text can span multiple\nlines.\n
+inputBoxInput1=some input text
+inputBoxAnswerTitle1=Your InputBox Answer
+inputBoxAnswerPrompt1=You entered: {0}
+
+inputBoxLabel2=Input box 2
+inputBoxButton2=Cance&llable input box
+inputBoxTitle2=Input Box
+inputBoxPrompt2=This is an example of an InputBox.\n\nNote that the InputBox text can span multiple\nlines.\nThis one has both OK and Cancel buttons.\n
+inputBoxInput2=some input text
+inputBoxAnswerTitle2=Your InputBox Answer
+inputBoxAnswerPrompt2=You entered: {0} and pressed {1}
+
+closeWindow=&Close Window
index 59dee8234ccfb90d0552484ab21f5e243135a244..196f3b9c3fa7f8b137d1428438e119a122681316 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
  */
 package jexer.demos;
 
-import java.util.*;
+import java.text.MessageFormat;
+import java.util.Date;
+import java.util.ResourceBundle;
 
-import jexer.*;
+import jexer.TAction;
+import jexer.TApplication;
+import jexer.TCalendar;
+import jexer.TField;
+import jexer.TMessageBox;
+import jexer.TWindow;
 import static jexer.TCommand.*;
 import static jexer.TKeypress.*;
 
@@ -39,6 +46,11 @@ import static jexer.TKeypress.*;
  */
 public class DemoTextFieldWindow extends TWindow {
 
+    /**
+     * Translated strings.
+     */
+    private static final ResourceBundle i18n = ResourceBundle.getBundle(DemoTextFieldWindow.class.getName());
+
     // ------------------------------------------------------------------------
     // Variables --------------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -71,37 +83,36 @@ public class DemoTextFieldWindow extends TWindow {
     DemoTextFieldWindow(final TApplication parent, final int flags) {
         // Construct a demo window.  X and Y don't matter because it
         // will be centered on screen.
-        super(parent, "Text Fields", 0, 0, 60, 20, flags);
+        super(parent, i18n.getString("windowTitle"), 0, 0, 60, 20, flags);
 
         int row = 1;
 
-        addLabel("Variable-width text field:", 1, row);
+        addLabel(i18n.getString("textField1"), 1, row);
         addField(35, row++, 15, false, "Field text");
-        addLabel("Fixed-width text field:", 1, row);
+        addLabel(i18n.getString("textField2"), 1, row);
         addField(35, row++, 15, true);
-        addLabel("Variable-width password:", 1, row);
+        addLabel(i18n.getString("textField3"), 1, row);
         addPasswordField(35, row++, 15, false);
-        addLabel("Fixed-width password:", 1, row);
+        addLabel(i18n.getString("textField4"), 1, row);
         addPasswordField(35, row++, 15, true, "hunter2");
-        addLabel("Very long text field:", 1, row);
+        addLabel(i18n.getString("textField5"), 1, row);
         TField selected = addField(35, row++, 40, false,
-            "Very very long field text that should be outside the window");
+            i18n.getString("textField6"));
         row += 1;
 
         calendar = addCalendar(1, row++,
             new TAction() {
                 public void DO() {
-                    getApplication().messageBox("Calendar",
-                        "You selected the following date:\n" +
-                        "\n" +
-                        new Date(calendar.getValue().getTimeInMillis()) +
-                        "\n",
+                    getApplication().messageBox(i18n.getString("calendarTitle"),
+                        MessageFormat.format(i18n.getString("calendarMessage"),
+                            new Date(calendar.getValue().getTimeInMillis())),
                         TMessageBox.Type.OK);
                 }
             }
         );
 
-        addButton("&Close Window", (getWidth() - 14) / 2, getHeight() - 4,
+        addButton(i18n.getString("closeWindow"),
+            (getWidth() - 14) / 2, getHeight() - 4,
             new TAction() {
                 public void DO() {
                     getApplication().closeWindow(DemoTextFieldWindow.this);
@@ -111,11 +122,15 @@ public class DemoTextFieldWindow extends TWindow {
 
         activate(selected);
 
-        statusBar = newStatusBar("Text fields");
-        statusBar.addShortcutKeypress(kbF1, cmHelp, "Help");
-        statusBar.addShortcutKeypress(kbF2, cmShell, "Shell");
-        statusBar.addShortcutKeypress(kbF3, cmOpen, "Open");
-        statusBar.addShortcutKeypress(kbF10, cmExit, "Exit");
+        statusBar = newStatusBar(i18n.getString("statusBar"));
+        statusBar.addShortcutKeypress(kbF1, cmHelp,
+            i18n.getString("statusBarHelp"));
+        statusBar.addShortcutKeypress(kbF2, cmShell,
+            i18n.getString("statusBarShell"));
+        statusBar.addShortcutKeypress(kbF3, cmOpen,
+            i18n.getString("statusBarOpen"));
+        statusBar.addShortcutKeypress(kbF10, cmExit,
+            i18n.getString("statusBarExit"));
     }
 
 }
diff --git a/src/jexer/demos/DemoTextFieldWindow.properties b/src/jexer/demos/DemoTextFieldWindow.properties
new file mode 100644 (file)
index 0000000..5b42990
--- /dev/null
@@ -0,0 +1,17 @@
+windowTitle=Text Fields
+
+statusBar=Text fields
+statusBarHelp=Help
+statusBarShell=Shell
+statusBarOpen=Open
+statusBarExit=Exit
+
+textField1=Variable-width text field:
+textField2=Fixed-width text field:
+textField3=Variable-width password:
+textField4=Fixed-width password:
+textField5=Very long text field:
+textField6=Very very long field text that should be outside the window
+calendarTitle=Calendar
+calendarMessage=You selected the following date:\n\n{0}\n
+closeWindow=&Close Window
index 969ca6381aa79bfc6fe0b9c5f1e41cfe5d27cb20..afa2a0ea2dd0ee833c94a1e6826116ae7319d4a9 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
  */
 package jexer.demos;
 
-import jexer.*;
-import jexer.event.*;
-import jexer.menu.*;
+import java.util.ResourceBundle;
+
+import jexer.TAction;
+import jexer.TApplication;
+import jexer.TText;
+import jexer.TWidget;
+import jexer.TWindow;
+import jexer.event.TResizeEvent;
+import jexer.menu.TMenu;
 import static jexer.TCommand.*;
 import static jexer.TKeypress.*;
 
@@ -39,6 +45,11 @@ import static jexer.TKeypress.*;
  */
 public class DemoTextWindow extends TWindow {
 
+    /**
+     * Translated strings.
+     */
+    private static final ResourceBundle i18n = ResourceBundle.getBundle(DemoTextWindow.class.getName());
+
     // ------------------------------------------------------------------------
     // Variables --------------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -65,35 +76,39 @@ public class DemoTextWindow extends TWindow {
         super(parent, title, 0, 0, 44, 22, RESIZABLE);
         textField = addText(text, 1, 3, 40, 16);
 
-        addButton("&Left", 1, 1, new TAction() {
+        addButton(i18n.getString("left"), 1, 1, new TAction() {
                 public void DO() {
                     textField.leftJustify();
                 }
         });
 
-        addButton("&Center", 10, 1, new TAction() {
+        addButton(i18n.getString("center"), 10, 1, new TAction() {
                 public void DO() {
                     textField.centerJustify();
                 }
         });
 
-        addButton("&Right", 21, 1, new TAction() {
+        addButton(i18n.getString("right"), 21, 1, new TAction() {
                 public void DO() {
                     textField.rightJustify();
                 }
         });
 
-        addButton("&Full", 31, 1, new TAction() {
+        addButton(i18n.getString("full"), 31, 1, new TAction() {
                 public void DO() {
                     textField.fullJustify();
                 }
         });
 
-        statusBar = newStatusBar("Reflowable text window");
-        statusBar.addShortcutKeypress(kbF1, cmHelp, "Help");
-        statusBar.addShortcutKeypress(kbF2, cmShell, "Shell");
-        statusBar.addShortcutKeypress(kbF3, cmOpen, "Open");
-        statusBar.addShortcutKeypress(kbF10, cmExit, "Exit");
+        statusBar = newStatusBar(i18n.getString("statusBar"));
+        statusBar.addShortcutKeypress(kbF1, cmHelp,
+            i18n.getString("statusBarHelp"));
+        statusBar.addShortcutKeypress(kbF2, cmShell,
+            i18n.getString("statusBarShell"));
+        statusBar.addShortcutKeypress(kbF3, cmOpen,
+            i18n.getString("statusBarOpen"));
+        statusBar.addShortcutKeypress(kbF10, cmExit,
+            i18n.getString("statusBarExit"));
     }
 
     /**
@@ -102,7 +117,7 @@ public class DemoTextWindow extends TWindow {
      * @param parent the main application
      */
     public DemoTextWindow(final TApplication parent) {
-        this(parent, "Text Area",
+        this(parent, i18n.getString("windowTitle"),
 "This is an example of a reflowable text field.  Some example text follows.\n" +
 "\n" +
 "Notice that some menu items should be disabled when this window has focus.\n" +
diff --git a/src/jexer/demos/DemoTextWindow.properties b/src/jexer/demos/DemoTextWindow.properties
new file mode 100644 (file)
index 0000000..873a56f
--- /dev/null
@@ -0,0 +1,12 @@
+windowTitle=Text Area
+
+statusBar=Reflowable text window
+statusBarHelp=Help
+statusBarShell=Shell
+statusBarOpen=Open
+statusBarExit=Exit
+
+left=&Left
+center=&Center
+right=&Right
+full=&Full
index e31ef765ba7b9df1386996ed7dba811b3c42dac7..479895127edd6d3ab5e20dbd03e2cfa96720966b 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
 package jexer.demos;
 
 import java.io.IOException;
+import java.util.ResourceBundle;
 
-import jexer.*;
-import jexer.event.*;
-import jexer.ttree.*;
+import jexer.TApplication;
+import jexer.TWidget;
+import jexer.TWindow;
+import jexer.event.TResizeEvent;
+import jexer.ttree.TDirectoryTreeItem;
+import jexer.ttree.TTreeViewWidget;
 import static jexer.TCommand.*;
 import static jexer.TKeypress.*;
 
@@ -41,6 +45,11 @@ import static jexer.TKeypress.*;
  */
 public class DemoTreeViewWindow extends TWindow {
 
+    /**
+     * Translated strings.
+     */
+    private static final ResourceBundle i18n = ResourceBundle.getBundle(DemoTreeViewWindow.class.getName());
+
     // ------------------------------------------------------------------------
     // Variables --------------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -61,17 +70,22 @@ public class DemoTreeViewWindow extends TWindow {
      * @throws IOException if a java.io operation throws
      */
     public DemoTreeViewWindow(final TApplication parent) throws IOException {
-        super(parent, "Tree View", 0, 0, 44, 16, TWindow.RESIZABLE);
+        super(parent, i18n.getString("windowTitle"), 0, 0, 44, 16,
+            TWindow.RESIZABLE);
 
         // Load the treeview with "stuff"
         treeView = addTreeViewWidget(1, 1, 40, 12);
         new TDirectoryTreeItem(treeView, ".", true);
 
-        statusBar = newStatusBar("Treeview demonstration");
-        statusBar.addShortcutKeypress(kbF1, cmHelp, "Help");
-        statusBar.addShortcutKeypress(kbF2, cmShell, "Shell");
-        statusBar.addShortcutKeypress(kbF3, cmOpen, "Open");
-        statusBar.addShortcutKeypress(kbF10, cmExit, "Exit");
+        statusBar = newStatusBar(i18n.getString("statusBar"));
+        statusBar.addShortcutKeypress(kbF1, cmHelp,
+            i18n.getString("statusBarHelp"));
+        statusBar.addShortcutKeypress(kbF2, cmShell,
+            i18n.getString("statusBarShell"));
+        statusBar.addShortcutKeypress(kbF3, cmOpen,
+            i18n.getString("statusBarOpen"));
+        statusBar.addShortcutKeypress(kbF10, cmExit,
+            i18n.getString("statusBarExit"));
     }
 
     // ------------------------------------------------------------------------
diff --git a/src/jexer/demos/DemoTreeViewWindow.properties b/src/jexer/demos/DemoTreeViewWindow.properties
new file mode 100644 (file)
index 0000000..d63b24e
--- /dev/null
@@ -0,0 +1,7 @@
+windowTitle=Tree View
+
+statusBar=Treeview demonstration
+statusBarHelp=Help
+statusBarShell=Shell
+statusBarOpen=Open
+statusBarExit=Exit
index 93e8597eb93753e99639d89e5b2260248eacd341..520f5b07f6780c465dc5299e56fa2ead00270233 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -28,7 +28,6 @@
  */
 package jexer.demos;
 
-
 import jexer.*;
 
 /**
index 0393860a3b0addfebf0e3b25004d70ca31c2b7aa..60ef7ec2caf23210857d92edf56288676e58d826 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
  */
 package jexer.demos;
 
-import java.io.*;
-import java.util.*;
+import java.io.File;
+import java.io.IOException;
+import java.util.ResourceBundle;
+import java.util.Scanner;
 
-import jexer.*;
-import jexer.event.*;
-import jexer.menu.*;
+import jexer.TAction;
+import jexer.TApplication;
+import jexer.TWindow;
+import jexer.event.TMenuEvent;
+import jexer.menu.TMenu;
 
 /**
  * The demo application itself.
  */
 public class DesktopDemoApplication extends TApplication {
 
+    /**
+     * Translated strings.
+     */
+    private static ResourceBundle i18n = ResourceBundle.getBundle(DesktopDemoApplication.class.getName());
+
     // ------------------------------------------------------------------------
     // Constructors -----------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -53,7 +62,7 @@ public class DesktopDemoApplication extends TApplication {
     public DesktopDemoApplication(final BackendType backendType) throws Exception {
         super(backendType);
         addAllWidgets();
-        getBackend().setTitle("Jexer Demo Application");
+        getBackend().setTitle(i18n.getString("applicationTitle"));
     }
 
     // ------------------------------------------------------------------------
@@ -119,14 +128,14 @@ public class DesktopDemoApplication extends TApplication {
         final DesktopDemo desktop = new DesktopDemo(this);
         setDesktop(desktop);
 
-        desktop.addButton("&Remove HATCH", 2, 5,
+        desktop.addButton(i18n.getString("removeHatch"), 2, 5,
             new TAction() {
                 public void DO() {
                     desktop.drawHatch = false;
                 }
             }
         );
-        desktop.addButton("&Show HATCH", 2, 8,
+        desktop.addButton(i18n.getString("showHatch"), 2, 8,
             new TAction() {
                 public void DO() {
                     desktop.drawHatch = true;
@@ -134,58 +143,60 @@ public class DesktopDemoApplication extends TApplication {
             }
         );
 
-        final TWindow windowA = addWindow("Window A", 25, 14);
-        final TWindow windowB = addWindow("Window B", 25, 14);
-        windowA.addButton("&Show Window B", 2, 2,
+        final TWindow windowA = addWindow(i18n.getString("windowATitle"),
+            25, 14);
+        final TWindow windowB = addWindow(i18n.getString("windowBTitle"),
+            25, 14);
+        windowA.addButton(i18n.getString("showWindowB"), 2, 2,
             new TAction() {
                 public void DO() {
                     windowB.show();
                 }
             }
         );
-        windowA.addButton("H&ide Window B", 2, 4,
+        windowA.addButton(i18n.getString("hideWindowB"), 2, 4,
             new TAction() {
                 public void DO() {
                     windowB.hide();
                 }
             }
         );
-        windowA.addButton("&Maximize Window B", 2, 6,
+        windowA.addButton(i18n.getString("maximizeWindowB"), 2, 6,
             new TAction() {
                 public void DO() {
                     windowB.maximize();
                 }
             }
         );
-        windowA.addButton("&Restore Window B", 2, 8,
+        windowA.addButton(i18n.getString("restoreWindowB"), 2, 8,
             new TAction() {
                 public void DO() {
                     windowB.restore();
                 }
             }
         );
-        windowB.addButton("&Show Window A", 2, 2,
+        windowB.addButton(i18n.getString("showWindowA"), 2, 2,
             new TAction() {
                 public void DO() {
                     windowA.show();
                 }
             }
         );
-        windowB.addButton("H&ide Window A", 2, 4,
+        windowB.addButton(i18n.getString("hideWindowA"), 2, 4,
             new TAction() {
                 public void DO() {
                     windowA.hide();
                 }
             }
         );
-        windowB.addButton("&Maximize Window A", 2, 6,
+        windowB.addButton(i18n.getString("maximizeWindowA"), 2, 6,
             new TAction() {
                 public void DO() {
                     windowA.maximize();
                 }
             }
         );
-        windowB.addButton("&Restore Window A", 2, 8,
+        windowB.addButton(i18n.getString("restoreWindowA"), 2, 8,
             new TAction() {
                 public void DO() {
                     windowA.restore();
@@ -193,40 +204,41 @@ public class DesktopDemoApplication extends TApplication {
             }
         );
 
-        desktop.addButton("S&how Window B", 25, 2,
+        desktop.addButton(i18n.getString("showWindowB"), 25, 2,
             new TAction() {
                 public void DO() {
                     windowB.show();
                 }
             }
         );
-        desktop.addButton("H&ide Window B", 25, 5,
+        desktop.addButton(i18n.getString("hideWindowB"), 25, 5,
             new TAction() {
                 public void DO() {
                     windowB.hide();
                 }
             }
         );
-        desktop.addButton("Sh&ow Window A", 25, 8,
+        desktop.addButton(i18n.getString("showWindowA"), 25, 8,
             new TAction() {
                 public void DO() {
                     windowA.show();
                 }
             }
         );
-        desktop.addButton("Hid&e Window A", 25, 11,
+        desktop.addButton(i18n.getString("hideWindowA"), 25, 11,
             new TAction() {
                 public void DO() {
                     windowA.hide();
                 }
             }
         );
-        desktop.addButton("&Create Window C", 25, 15,
+        desktop.addButton(i18n.getString("createWindowC"), 25, 15,
             new TAction() {
                 public void DO() {
                     final TWindow windowC = desktop.getApplication().addWindow(
-                        "Window C", 30, 20, TWindow.NOCLOSEBOX);
-                    windowC.addButton("&Close Me", 5, 5,
+                        i18n.getString("windowCTitle"), 30, 20,
+                        TWindow.NOCLOSEBOX);
+                    windowC.addButton(i18n.getString("closeMe"), 5, 5,
                         new TAction() {
                             public void DO() {
                                 windowC.close();
@@ -237,21 +249,20 @@ public class DesktopDemoApplication extends TApplication {
             }
         );
 
-        desktop.addButton("Enable focusFollowsMouse", 25, 18,
+        desktop.addButton(i18n.getString("enableFFM"), 25, 18,
             new TAction() {
                 public void DO() {
                     DesktopDemoApplication.this.setFocusFollowsMouse(true);
                 }
             }
         );
-        desktop.addButton("Disable focusFollowsMouse", 25, 21,
+        desktop.addButton(i18n.getString("disableFFM"), 25, 21,
             new TAction() {
                 public void DO() {
                     DesktopDemoApplication.this.setFocusFollowsMouse(false);
                 }
             }
         );
-
     }
 
 }
diff --git a/src/jexer/demos/DesktopDemoApplication.properties b/src/jexer/demos/DesktopDemoApplication.properties
new file mode 100644 (file)
index 0000000..85f7435
--- /dev/null
@@ -0,0 +1,19 @@
+applicationTitle=Demo Application
+
+removeHatch=Remove HATCH
+showHatch=Show HATCH
+closeMe=Close Me
+createWindowC=Create Window C
+disableFFM=Disable focusFollowsMouse
+enableFFM=Enable focusFollowsMouse
+hideWindowA=Hide Window A
+hideWindowB=Hide Window B
+maximizeWindowA=Maximize Window A
+maximizeWindowB=Maximize Window B
+restoreWindowA=Restore Window A
+restoreWindowB=Restore Window B
+showWindowA=Show Window A
+showWindowB=Show Window B
+windowATitle=Window A
+windowBTitle=Window B
+windowCTitle=Window C
index 45ce7d4c72e5b5b7258b467f216fde3ebb4d3b74..1305cddaafb690cd41251fe47a79f8b3afa868c7 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index 53b9ad252aaa3afd3f146e5e0ed42dc3f61737fd..60f6385343889a74ce7d77a593ad9ae5775235f5 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index d03ef01e55600e23a7a246fa77a1fd852d352e9d..220512fab1f8ccfee3443e23209b822241dbed4b 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index f49936910d95d68694c101623bb50998f35a904f..b56f08baa580fb022a0f91f644b51316a7539325 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index 5117846580f7c13142f955081c30dedf215ce25a..e2ff7c74b6a1125a19e91e15685f9f5b3b408452 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index 8c967c0706690fc7f12dd0041dc20c5aee4a089b..496d8bc06422baa3014a95a98ae953f60cd4325e 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index 472ef3e4ded053050ab29a00af2d909a21f9de13..ff9571084232f1891258c1b0af02758f231fabb1 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index dce2c90535be777f919fab3c024c11e3eb3f483d..e4541a3b9e0daac4a5061f9df1d89ad694df1c4f 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index 143845af3c0f09ff55ff959dd82776eeb1f195a8..8c6371e267b2c98069fe423ac1450481f56b6e7c 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index d65426261ed3df9e587b62135d7c55e0cd618197..3d8cdb0312494293b5e90bdeee39c5f21b32f659 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -179,8 +179,11 @@ public class TimeoutInputStream extends InputStream {
             // available.  If not, we throw ReadTimeoutException.
             long checkTime = System.currentTimeMillis();
             while (stream.available() == 0) {
-                long now = System.currentTimeMillis();
+                if (remaining > 0) {
+                    return (b.length - remaining);
+                }
 
+                long now = System.currentTimeMillis();
                 synchronized (this) {
                     if ((now - checkTime > timeoutMillis) || (cancel == true)) {
                         if (cancel == true) {
@@ -257,6 +260,10 @@ public class TimeoutInputStream extends InputStream {
             // available.  If not, we throw ReadTimeoutException.
             long checkTime = System.currentTimeMillis();
             while (stream.available() == 0) {
+                if (remaining > 0) {
+                    return (len - remaining);
+                }
+
                 long now = System.currentTimeMillis();
                 synchronized (this) {
                     if ((now - checkTime > timeoutMillis) || (cancel == true)) {
index 4c04cf10d9723521b6c9caf187fe10a835542181..37ad2bbe9987adc1d6d8bb3b1e840a9289d1b516 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index 3120635996e503fd43feb4bc24d3c031c43911bf..bfda60247a2f81ffe20afcd8599a94a06181cfe7 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -161,6 +161,7 @@ public class TMenu extends TWindow {
     @Override
     public void onMouseDown(final TMouseEvent mouse) {
         this.mouse = mouse;
+        super.onMouseDown(mouse);
 
         // Pass to children
         for (TWidget widget: getChildren()) {
@@ -341,7 +342,7 @@ public class TMenu extends TWindow {
         hLineXY(1 + 1, getHeight() - 1, getWidth() - 4, cHSide, background);
 
         // Draw a shadow
-        getScreen().drawBoxShadow(0, 0, getWidth(), getHeight());
+        drawBoxShadow(0, 0, getWidth(), getHeight());
     }
 
     // ------------------------------------------------------------------------
@@ -387,6 +388,21 @@ public class TMenu extends TWindow {
         return addItemInternal(id, label, null);
     }
 
+    /**
+     * Convenience function to add a menu item.
+     *
+     * @param id menu item ID.  Must be greater than 1024.
+     * @param label menu item label
+     * @param enabled default state for enabled
+     * @return the new menu item
+     */
+    public TMenuItem addItem(final int id, final String label,
+        final boolean enabled) {
+
+        assert (id >= 1024);
+        return addItemInternal(id, label, null, enabled);
+    }
+
     /**
      * Convenience function to add a custom menu item.
      *
index 63a5355e348f0e553215a67c88d45c9979a856d5..6e455d8e99c2bc2756ff0ae23302e73fdbc001ae 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -220,23 +220,23 @@ public class TMenuItem extends TWidget {
         }
 
         char cVSide = GraphicsChars.WINDOW_SIDE;
-        getScreen().vLineXY(0, 0, 1, cVSide, background);
-        getScreen().vLineXY(getWidth() - 1, 0, 1, cVSide, background);
+        vLineXY(0, 0, 1, cVSide, background);
+        vLineXY(getWidth() - 1, 0, 1, cVSide, background);
 
-        getScreen().hLineXY(1, 0, getWidth() - 2, ' ', menuColor);
-        getScreen().putStringXY(2, 0, mnemonic.getRawLabel(), menuColor);
+        hLineXY(1, 0, getWidth() - 2, ' ', menuColor);
+        putStringXY(2, 0, mnemonic.getRawLabel(), menuColor);
         if (key != null) {
             String keyLabel = key.toString();
-            getScreen().putStringXY((getWidth() - keyLabel.length() - 2), 0,
-                keyLabel, menuColor);
+            putStringXY((getWidth() - keyLabel.length() - 2), 0, keyLabel,
+                menuColor);
         }
         if (mnemonic.getShortcutIdx() >= 0) {
-            getScreen().putCharXY(2 + mnemonic.getShortcutIdx(), 0,
-                mnemonic.getShortcut(), menuMnemonicColor);
+            putCharXY(2 + mnemonic.getShortcutIdx(), 0, mnemonic.getShortcut(),
+                menuMnemonicColor);
         }
         if (checked) {
             assert (checkable);
-            getScreen().putCharXY(1, 0, GraphicsChars.CHECK, menuColor);
+            putCharXY(1, 0, GraphicsChars.CHECK, menuColor);
         }
 
     }
index daae13ac66b1ac3fa74d3fe2ac4dbc89bc3cf1e8..0528e5d73207cd8d8cd5abcca72c167b6c757149 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -65,11 +65,9 @@ public class TMenuSeparator extends TMenuItem {
     public void draw() {
         CellAttributes background = getTheme().getColor("tmenu");
 
-        getScreen().putCharXY(0, 0, GraphicsChars.CP437[0xC3], background);
-        getScreen().putCharXY(getWidth() - 1, 0, GraphicsChars.CP437[0xB4],
-            background);
-        getScreen().hLineXY(1, 0, getWidth() - 2, GraphicsChars.SINGLE_BAR,
-            background);
+        putCharXY(0, 0, GraphicsChars.CP437[0xC3], background);
+        putCharXY(getWidth() - 1, 0, GraphicsChars.CP437[0xB4], background);
+        hLineXY(1, 0, getWidth() - 2, GraphicsChars.SINGLE_BAR, background);
     }
 
 }
index b61ca83cecbd0ff0af48b6d4cd05e60d4a3293f8..547711bcf5f4a39c3f261010f1be9ca0f960c174 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -163,8 +163,7 @@ public class TSubMenu extends TMenuItem {
         }
 
         // Add the arrow
-        getScreen().putCharXY(getWidth() - 2, 0, GraphicsChars.CP437[0x10],
-            menuColor);
+        putCharXY(getWidth() - 2, 0, GraphicsChars.CP437[0x10], menuColor);
     }
 
     /**
index 6781106fa6223193ff3eeccff4cd4af070122578..2c10393c0615ca25c744fbc2128d659d1bb08e3d 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index 1764b88303ebf416806a6eb0f490d183e36ba8c8..056a7dc8e4e10ad2a4d8981f6c1668e1e0422c80 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index 994655c2ea31ffe234b1c00ef4d762ae50c1e5e9..905c52a799e732588975af64099864a97d4621ed 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index b368d3469bb65c6acc609c6ae7109e4f34bb3a25..3c5b3077ba591ff706b564ed3dd5955675e61240 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index 34e715f34037c556f78dcde4f0851595b9906d39..ac8a2782b0ed672e2166026ffe46439a4bc2681b 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index 29ff720ad242057afb644fc1cba97947be1062a4..5d738fb09f847b7123ad735f19ea30d0277d91c7 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index 46c48fa995447bb0caa2269fdc6cc21bc5c11ecf..7a5a752b981c218363e83a80df69d60b1b067990 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index bd8e91f7a48fa575747fca4e7a0cfb8e22c4248a..9b0453860327b67ecfa5837ffa4966b5008854fe 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index 4a1ae903a33e0447d5694619a088829f4c6d14f2..4f601556c5d0ffd4c94d7fed98698f2c11247d63 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index 400de9a8a73db37446da0976dd4f001966a4dee3..965c38f84a421986cd753c8b42f74cdfcd7228bf 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index 11fa39d9dee5545592426374f9cff1c6e189d016..ffb11aa1a520d9b37ca76eee98f776f9509794d9 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index 38af57d1080920a60da7f32fbbbb801b7f1b59d9..8bf5199b4c596280dae88b54a6e196ffe230acff 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index 5110db0edbdb925b3fc93368d919dac401702f68..bca81bb701a8d82482c39fffa569da6bc33dba0a 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index 66036d7409ec935f2659aeab03cca81205428dc9..7c76713a715cf021e3d882717f434484bc9b0334 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index 7a47921ddad2e2f365330e3e0f38da08cfe3d501..d0c9e2d73f769658a0c7dcdd89ac2e51be3d7f85 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index 9376828ee7689411ff7492de501d7109bc892445..b08a18d5c3070ab207367ab3e8817eb955c1ddbe 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
 package jexer.tterminal;
 
 import java.io.BufferedOutputStream;
+import java.io.CharArrayWriter;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
 import java.io.Reader;
 import java.io.UnsupportedEncodingException;
 import java.io.Writer;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.LinkedList;
 import java.util.List;
 
 import jexer.TKeypress;
@@ -252,12 +253,17 @@ public class ECMA48 implements Runnable {
     /**
      * The scrollback buffer characters + attributes.
      */
-    private volatile List<DisplayLine> scrollback;
+    private volatile ArrayList<DisplayLine> scrollback;
 
     /**
      * The raw display buffer characters + attributes.
      */
-    private volatile List<DisplayLine> display;
+    private volatile ArrayList<DisplayLine> display;
+
+    /**
+     * The maximum number of lines in the scrollback buffer.
+     */
+    private int maxScrollback = 10000;
 
     /**
      * The terminal's input.  For type == XTERM, this is an InputStreamReader
@@ -585,8 +591,8 @@ public class ECMA48 implements Runnable {
 
         csiParams         = new ArrayList<Integer>();
         tabStops          = new ArrayList<Integer>();
-        scrollback        = new LinkedList<DisplayLine>();
-        display           = new LinkedList<DisplayLine>();
+        scrollback        = new ArrayList<DisplayLine>();
+        display           = new ArrayList<DisplayLine>();
 
         this.type         = type;
         if (inputStream instanceof TimeoutInputStream) {
@@ -697,7 +703,7 @@ public class ECMA48 implements Runnable {
                                 ch = readBuffer[i];
                             }
 
-                            consume((char)ch);
+                            consume((char) ch);
                         }
                     }
                     // Permit my enclosing UI to know that I updated.
@@ -707,8 +713,29 @@ public class ECMA48 implements Runnable {
                 }
                 // System.err.println("end while loop"); System.err.flush();
             } catch (IOException e) {
-                e.printStackTrace();
                 done = true;
+
+                // This is an unusual case.  We want to see the stack trace,
+                // but it is related to the spawned process rather than the
+                // actual UI.  We will generate the stack trace, and consume
+                // it as though it was emitted by the shell.
+                CharArrayWriter writer= new CharArrayWriter();
+                // Send a ST and RIS to clear the emulator state.
+                try {
+                    writer.write("\033\\\033c");
+                    writer.write("\n-----------------------------------\n");
+                    e.printStackTrace(new PrintWriter(writer));
+                    writer.write("\n-----------------------------------\n");
+                } catch (IOException e2) {
+                    // SQUASH
+                }
+                char [] stackTrace = writer.toCharArray();
+                for (int i = 0; i < stackTrace.length; i++) {
+                    if (stackTrace[i] == '\n') {
+                        consume('\r');
+                    }
+                    consume(stackTrace[i]);
+                }
             }
 
         } // while ((done == false) && (stopReaderThread == false))
@@ -889,7 +916,7 @@ public class ECMA48 implements Runnable {
             try {
                 readerThread.join(1000);
             } catch (InterruptedException e) {
-                e.printStackTrace();
+                // SQUASH
             }
         }
 
@@ -1234,7 +1261,12 @@ public class ECMA48 implements Runnable {
     private void newDisplayLine() {
         // Scroll the top line off into the scrollback buffer
         scrollback.add(display.get(0));
+        if (scrollback.size() > maxScrollback) {
+            scrollback.remove(0);
+            scrollback.trimToSize();
+        }
         display.remove(0);
+        display.trimToSize();
         DisplayLine line = new DisplayLine(currentState.attr);
         line.setReverseColor(reverseVideo);
         display.add(line);
@@ -2415,7 +2447,7 @@ public class ECMA48 implements Runnable {
             display.size());
         List<DisplayLine> displayMiddle = display.subList(regionBottom + 1
             - remaining, regionBottom + 1);
-        display = new LinkedList<DisplayLine>(displayTop);
+        display = new ArrayList<DisplayLine>(displayTop);
         display.addAll(displayMiddle);
         for (int i = 0; i < n; i++) {
             DisplayLine line = new DisplayLine(currentState.attr);
@@ -2456,7 +2488,7 @@ public class ECMA48 implements Runnable {
             display.size());
         List<DisplayLine> displayMiddle = display.subList(regionTop,
             regionTop + remaining);
-        display = new LinkedList<DisplayLine>(displayTop);
+        display = new ArrayList<DisplayLine>(displayTop);
         for (int i = 0; i < n; i++) {
             DisplayLine line = new DisplayLine(currentState.attr);
             line.setReverseColor(reverseVideo);
index 93e1b3cbd7946955315abc970d49b83b4416034d..b92d1535dfd4e82f2c79d59805af56ceda6ec373 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index 8c54b386753e60b1a6f8c70d35170126fd7c6cbc..5f265ee5d32783654f67fe690bb3c666af1fbf5b 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index 901ce8590a78095bdf00a5831f86331b5b84d9e9..759bfb7a857ff3878c7986c63a26adafb1468f3f 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -248,7 +248,7 @@ public class TTreeItem extends TWidget {
         }
 
         // Blank out the background
-        getScreen().hLineXY(0, 0, getWidth(), ' ', color);
+        hLineXY(0, 0, getWidth(), ' ', color);
 
         String line = prefix;
         if (level > 0) {
@@ -264,20 +264,17 @@ public class TTreeItem extends TWidget {
                 line += " ";
             }
         }
-        getScreen().putStringXY(offset, 0, line, color);
+        putStringXY(offset, 0, line, color);
         if (selected) {
-            getScreen().putStringXY(offset + line.length(), 0, text,
-                selectedColor);
+            putStringXY(offset + line.length(), 0, text, selectedColor);
         } else {
-            getScreen().putStringXY(offset + line.length(), 0, text, textColor);
+            putStringXY(offset + line.length(), 0, text, textColor);
         }
         if ((level > 0) && (expandable)) {
             if (expanded) {
-                getScreen().putCharXY(offset + getExpanderX(), 0, '-',
-                    expanderColor);
+                putCharXY(offset + getExpanderX(), 0, '-', expanderColor);
             } else {
-                getScreen().putCharXY(offset + getExpanderX(), 0, '+',
-                    expanderColor);
+                putCharXY(offset + getExpanderX(), 0, '+', expanderColor);
             }
         }
     }
index acc89249080fb8f9fe85feb22c859db38b5026c1..88abd7037b93f5c39f0f8f247363f8176e75c206 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index adb9a5d8e354b1974d70843e608944e0dd4b5c8b..ceb50ab9c38c1a1a14730031b5247f2b38af3b4f 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index 2c642063812c2262677f12f9aeb0417149b7f400..e570900051c820e243ef891891bfc40a6a18ef89 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
index 72dc8da76649cd369e001d61c77c8bebbfafbf86..1e1fdfd0fcab836658eddf6751f136d74a15da34 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),