help system
authorKevin Lamonte <kevin.lamonte@gmail.com>
Tue, 26 Nov 2019 22:17:46 +0000 (16:17 -0600)
committerKevin Lamonte <kevin.lamonte@gmail.com>
Tue, 26 Nov 2019 22:17:46 +0000 (16:17 -0600)
23 files changed:
resources/help.xml [new file with mode: 0644]
src/jexer/TApplication.java
src/jexer/TApplication.properties
src/jexer/TEditorWidget.java
src/jexer/TExceptionDialog.properties
src/jexer/THelpWindow.java [new file with mode: 0644]
src/jexer/THelpWindow.properties [new file with mode: 0644]
src/jexer/TList.java
src/jexer/TTerminalWindow.java
src/jexer/TTerminalWindow.properties
src/jexer/TText.java
src/jexer/TWindow.java
src/jexer/bits/ColorTheme.java
src/jexer/bits/StringUtils.java
src/jexer/help/HelpFile.java [new file with mode: 0644]
src/jexer/help/HelpFile.properties [new file with mode: 0644]
src/jexer/help/Link.java [new file with mode: 0644]
src/jexer/help/THelpText.java [new file with mode: 0644]
src/jexer/help/TParagraph.java [new file with mode: 0644]
src/jexer/help/TWord.java [new file with mode: 0644]
src/jexer/help/Topic.java [new file with mode: 0644]
src/jexer/help/Topic.properties [new file with mode: 0644]
src/jexer/help/package-info.java [new file with mode: 0644]

diff --git a/resources/help.xml b/resources/help.xml
new file mode 100644 (file)
index 0000000..7c68c02
--- /dev/null
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<help>
+  <name>Jexer Help File</name>
+  <author>Kevin Lamonte</author>
+  <version>1.0.0</version>
+  <date>Jan 1, 2020</date>
+  <topics>
+    <topic title="Help">
+      <text>
+        This [window](Windows) does not have a specific help topic.
+        See [here](Help On Help) for general information on using the
+        help system.
+      </text>
+    </topic>
+    <topic title="Help On Help">
+      <text>
+        The #{help} system...
+
+      </text>
+    </topic>
+
+    <topic title="Menus">
+      <text>
+        #{Menus} do ...
+      </text>
+    </topic>
+
+    <topic title="Windows">
+      <text>
+        #{Windows} do ...
+      </text>
+    </topic>
+
+    <topic title="Editing Text">
+      <text>
+        The #{text editing} [window](Windows)...
+      </text>
+    </topic>
+
+    <topic title="Editing Tables">
+      <text></text>
+    </topic>
+
+    <topic title="Terminal Window">
+      <text>
+        The terminal window ...
+      </text>
+    </topic>
+
+    <topic title="Copyright Infomation">
+      <text>
+        Copyright (C) 2019 Kevin Lamonte
+
+        Available to all under the MIT License.
+    </text>
+    </topic>
+
+  </topics>
+</help>
index e7ba0f853af5e2e087bc9c25d0384bb1a3d06a38..e2c3cd685f3cfd41674a49da97565b8e94b309d7 100644 (file)
@@ -29,6 +29,7 @@
 package jexer;
 
 import java.io.File;
+import java.io.FileInputStream;
 import java.io.InputStream;
 import java.io.IOException;
 import java.io.OutputStream;
@@ -62,6 +63,8 @@ import jexer.backend.Screen;
 import jexer.backend.SwingBackend;
 import jexer.backend.ECMA48Backend;
 import jexer.backend.TWindowBackend;
+import jexer.help.HelpFile;
+import jexer.help.Topic;
 import jexer.menu.TMenu;
 import jexer.menu.TMenuItem;
 import jexer.menu.TSubMenu;
@@ -346,6 +349,16 @@ public class TApplication implements Runnable {
      */
     private int screenSelectionY1;
 
+    /**
+     * The help file data.  Note package private access.
+     */
+    HelpFile helpFile;
+
+    /**
+     * The stack of help topics.  Note package private access.
+     */
+    ArrayList<Topic> helpTopics = new ArrayList<Topic>();
+
     /**
      * WidgetEventHandler is the main event consumer loop.  There are at most
      * two such threads in existence: the primary for normal case and a
@@ -794,6 +807,14 @@ public class TApplication implements Runnable {
             }
         }
 
+        // Load the help system
+        try {
+            ClassLoader loader = Thread.currentThread().getContextClassLoader();
+            helpFile = new HelpFile();
+            helpFile.load(loader.getResourceAsStream("help.xml"));
+        } catch (Exception e) {
+            new TExceptionDialog(this, e);
+        }
     }
 
     // ------------------------------------------------------------------------
@@ -925,6 +946,15 @@ public class TApplication implements Runnable {
             return true;
         }
 
+        if (command.equals(cmHelp)) {
+            if (getActiveWindow() != null) {
+                new THelpWindow(this, getActiveWindow().getHelpTopic());
+            } else {
+                new THelpWindow(this);
+            }
+            return true;
+        }
+
         if (command.equals(cmShell)) {
             openTerminal(0, 0, TWindow.RESIZABLE);
             return true;
@@ -976,6 +1006,62 @@ public class TApplication implements Runnable {
             return true;
         }
 
+        if (menu.getId() == TMenu.MID_HELP_HELP) {
+            new THelpWindow(this, THelpWindow.HELP_HELP);
+            return true;
+        }
+
+        if (menu.getId() == TMenu.MID_HELP_CONTENTS) {
+            new THelpWindow(this, helpFile.getTableOfContents());
+            return true;
+        }
+
+        if (menu.getId() == TMenu.MID_HELP_INDEX) {
+            new THelpWindow(this, helpFile.getIndex());
+            return true;
+        }
+
+        if (menu.getId() == TMenu.MID_HELP_SEARCH) {
+            TInputBox inputBox = inputBox(i18n.
+                getString("searchHelpInputBoxTitle"),
+                i18n.getString("searchHelpInputBoxCaption"), "",
+                TInputBox.Type.OKCANCEL);
+            if (inputBox.isOk()) {
+                new THelpWindow(this,
+                    helpFile.getSearchResults(inputBox.getText()));
+            }
+            return true;
+        }
+
+        if (menu.getId() == TMenu.MID_HELP_PREVIOUS) {
+            if (helpTopics.size() > 1) {
+                Topic previous = helpTopics.remove(helpTopics.size() - 2);
+                helpTopics.remove(helpTopics.size() - 1);
+                new THelpWindow(this, previous);
+            } else {
+                new THelpWindow(this, helpFile.getTableOfContents());
+            }
+            return true;
+        }
+
+        if (menu.getId() == TMenu.MID_HELP_ACTIVE_FILE) {
+            try {
+                List<String> filters = new ArrayList<String>();
+                filters.add("^.*\\.[Xx][Mm][Ll]$");
+                String filename = fileOpenBox(".", TFileOpenBox.Type.OPEN,
+                    filters);
+                if (filename != null) {
+                    helpTopics = new ArrayList<Topic>();
+                    helpFile = new HelpFile();
+                    helpFile.load(new FileInputStream(filename));
+                }
+            } catch (Exception e) {
+                // Show this exception to the user.
+                new TExceptionDialog(this, e);
+            }
+            return true;
+        }
+
         if (menu.getId() == TMenu.MID_SHELL) {
             openTerminal(0, 0, TWindow.RESIZABLE);
             return true;
index 299c6a3a90501469469dc7a8550a3c8f42cf146d..57f7c595d85e030d7ab56ce724a3817a4d8f2260 100644 (file)
@@ -25,3 +25,6 @@ exitDialogText=Exit application?
 
 aboutDialogTitle=About
 aboutDialogText=Jexer Version {0}
+
+searchHelpInputBoxTitle=Search Help Topics
+searchHelpInputBoxCaption=Search help topics for (regex):
index 6d24a196af8881cd75c70455cdbae8b94dd2b618..6ff39e61308b04b2a9171e8e4aaa7517314a8ed3 100644 (file)
@@ -312,6 +312,12 @@ public class TEditorWidget extends TWidget implements EditMenuUser {
                 // Non-shifted navigation keys disable selection.
                 inSelection = false;
             }
+            if ((selectionColumn0 == selectionColumn1)
+                && (selectionLine0 == selectionLine1)
+            ) {
+                // The user clicked a spot and started typing.
+                inSelection = false;
+            }
         }
 
         if (keypress.equals(kbLeft)
index d07998cf2931c6956becc0d2a177f90d5a153d8c..9e5857a6deff14c2374b09c41ce5d1fc2362469d 100644 (file)
@@ -1,10 +1,10 @@
 windowTitle=Java Exception Caught
 statusBar=Exception
 
-captionLine1=An error has occurred.  This may be due to a programming bug, but
-captionLine2=could also be a correctable or temporary issue.  The stack trace
-captionLine3=is reported below.  If you wish to submit a bug report, please
-captionLine4=use the Save button to create a more detailed error log.
+captionLine1=An error has occurred.  This may be due to a programming bug, but could
+captionLine2=also be a correctable or temporary issue.  The stack trace is reported
+captionLine3=below.  If you wish to submit a bug report, please use the Save button
+captionLine4=to create a more detailed error log.
 
 exceptionString={0}: {1}
 
diff --git a/src/jexer/THelpWindow.java b/src/jexer/THelpWindow.java
new file mode 100644 (file)
index 0000000..ee7ce54
--- /dev/null
@@ -0,0 +1,271 @@
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer;
+
+import java.util.ResourceBundle;
+
+import jexer.bits.CellAttributes;
+import jexer.event.TResizeEvent;
+import jexer.help.THelpText;
+import jexer.help.Topic;
+
+/**
+ * THelpWindow
+ */
+public class THelpWindow extends TWindow {
+
+    /**
+     * Translated strings.
+     */
+    private static final ResourceBundle i18n = ResourceBundle.getBundle(THelpWindow.class.getName());
+
+    // ------------------------------------------------------------------------
+    // Constants --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    // Default help topic keys.  Note package private access.
+    static String HELP_HELP                     = "Help On Help";
+
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * The help text window.
+     */
+    private THelpText helpText;
+
+    /**
+     * The "Contents" button.
+     */
+    private TButton contentsButton;
+
+    /**
+     * The "Index" button.
+     */
+    private TButton indexButton;
+
+    /**
+     * The "Previous" button.
+     */
+    private TButton previousButton;
+
+    /**
+     * The "Close" button.
+     */
+    private TButton closeButton;
+
+    /**
+     * The X position for the buttons.
+     */
+    private int buttonOffset = 14;
+
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Public constructor.
+     *
+     * @param application TApplication that manages this window
+     * @param topic the topic to start on
+     */
+    public THelpWindow(final TApplication application, final String topic) {
+        this (application, application.helpFile.getTopic(topic));
+    }
+
+    /**
+     * Public constructor.
+     *
+     * @param application TApplication that manages this window
+     * @param topic the topic to start on
+     */
+    public THelpWindow(final TApplication application, final Topic topic) {
+        super(application, i18n.getString("windowTitle"),
+            1, 1, 78, 22, CENTERED | RESIZABLE);
+
+        setMinimumWindowHeight(16);
+        setMinimumWindowWidth(30);
+
+        helpText = new THelpText(this, topic, 1, 1,
+            getWidth() - buttonOffset - 4, getHeight() - 4);
+
+        setHelpTopic(topic);
+
+        // Buttons
+        previousButton = addButton(i18n.getString("previousButton"),
+            getWidth() - buttonOffset, 4,
+            new TAction() {
+                public void DO() {
+                    if (application.helpTopics.size() > 1) {
+                        Topic previous = application.helpTopics.remove(
+                            application.helpTopics.size() - 2);
+                        application.helpTopics.remove(application.
+                            helpTopics.size() - 1);
+                        setHelpTopic(previous);
+                    }
+                }
+            });
+
+        contentsButton = addButton(i18n.getString("contentsButton"),
+            getWidth() - buttonOffset, 6,
+            new TAction() {
+                public void DO() {
+                    setHelpTopic(application.helpFile.getTableOfContents());
+                }
+            });
+
+        indexButton = addButton(i18n.getString("indexButton"),
+            getWidth() - buttonOffset, 8,
+            new TAction() {
+                public void DO() {
+                    setHelpTopic(application.helpFile.getIndex());
+                }
+            });
+
+        closeButton = addButton(i18n.getString("closeButton"),
+            getWidth() - buttonOffset, 10,
+            new TAction() {
+                public void DO() {
+                    // Don't copy anything, just close the window.
+                    THelpWindow.this.close();
+                }
+            });
+
+        // Save this for last: make the close button default action.
+        activate(closeButton);
+
+    }
+
+    /**
+     * Public constructor.
+     *
+     * @param application TApplication that manages this window
+     */
+    public THelpWindow(final TApplication application) {
+        this(application, HELP_HELP);
+    }
+
+    // ------------------------------------------------------------------------
+    // Event handlers ---------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Handle window/screen resize events.
+     *
+     * @param event resize event
+     */
+    @Override
+    public void onResize(final TResizeEvent event) {
+        if (event.getType() == TResizeEvent.Type.WIDGET) {
+
+            previousButton.setX(getWidth() - buttonOffset);
+            contentsButton.setX(getWidth() - buttonOffset);
+            indexButton.setX(getWidth() - buttonOffset);
+            closeButton.setX(getWidth() - buttonOffset);
+
+            helpText.setDimensions(1, 1, getWidth() - buttonOffset - 4,
+                getHeight() - 4);
+            helpText.onResize(new TResizeEvent(TResizeEvent.Type.WIDGET,
+                    helpText.getWidth(), helpText.getHeight()));
+
+            return;
+        } else {
+            super.onResize(event);
+        }
+    }
+
+    // ------------------------------------------------------------------------
+    // TWindow ----------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Retrieve the background color.
+     *
+     * @return the background color
+     */
+    @Override
+    public final CellAttributes getBackground() {
+        return getTheme().getColor("thelpwindow.background");
+    }
+
+    /**
+     * Retrieve the border color.
+     *
+     * @return the border color
+     */
+    @Override
+    public CellAttributes getBorder() {
+        if (inWindowMove) {
+            return getTheme().getColor("thelpwindow.windowmove");
+        }
+        return getTheme().getColor("thelpwindow.background");
+    }
+
+    /**
+     * Retrieve the color used by the window movement/sizing controls.
+     *
+     * @return the color used by the zoom box, resize bar, and close box
+     */
+    @Override
+    public CellAttributes getBorderControls() {
+        return getTheme().getColor("thelpwindow.border");
+    }
+
+    // ------------------------------------------------------------------------
+    // THelpWindow ------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Set the topic to display.
+     *
+     * @param topic the topic to display
+     */
+    public void setHelpTopic(final String topic) {
+        setHelpTopic(getApplication().helpFile.getTopic(topic));
+    }
+
+    /**
+     * Set the topic to display.
+     *
+     * @param topic the topic to display
+     */
+    private void setHelpTopic(final Topic topic) {
+        boolean separator = true;
+        if ((topic == getApplication().helpFile.getTableOfContents())
+            || (topic == getApplication().helpFile.getIndex())
+        ) {
+            separator = false;
+        }
+
+        getApplication().helpTopics.add(topic);
+        helpText.setTopic(topic, separator);
+    }
+
+}
diff --git a/src/jexer/THelpWindow.properties b/src/jexer/THelpWindow.properties
new file mode 100644 (file)
index 0000000..2b25484
--- /dev/null
@@ -0,0 +1,5 @@
+windowTitle=Help
+previousButton=Pre&vious
+contentsButton=Co&ntents
+indexButton=\ &Index\ \ 
+closeButton=\ C&lose\ \ 
index 6b0a205bf422e2bd18cc186f226777d6c0c98084..12e0b8a33cce977e93ce4a6fd30080adeb75e8dc 100644 (file)
@@ -398,6 +398,9 @@ public class TList extends TScrollableWidget {
         int topY = 0;
         for (int i = begin; i < strings.size(); i++) {
             String line = strings.get(i);
+            if (line == null) {
+                line = "";
+            }
             if (getHorizontalValue() < line.length()) {
                 line = line.substring(getHorizontalValue());
             } else {
index a22625e8e95f5287c8ba3d727ded3e3633336bb8..754b7a512d6f7581216a78b76c2a8a7be3838dcb 100644 (file)
@@ -35,6 +35,7 @@ import jexer.event.TKeypressEvent;
 import jexer.event.TMenuEvent;
 import jexer.event.TMouseEvent;
 import jexer.event.TResizeEvent;
+import static jexer.TCommand.*;
 import static jexer.TKeypress.*;
 
 /**
@@ -141,7 +142,11 @@ public class TTerminalWindow extends TScrollableWindow {
         addShortcutKeys();
 
         // Add shortcut text
-        newStatusBar(i18n.getString("statusBarRunning"));
+        TStatusBar statusBar = newStatusBar(i18n.getString("statusBarRunning"));
+        statusBar.addShortcutKeypress(kbF1, cmHelp,
+            i18n.getString("statusBarHelp"));
+        statusBar.addShortcutKeypress(kbF10, cmMenu,
+            i18n.getString("statusBarMenu"));
 
         // Spin it up
         terminal = new TTerminalWidget(this, 0, 0, command, new TAction() {
@@ -193,7 +198,11 @@ public class TTerminalWindow extends TScrollableWindow {
         addShortcutKeys();
 
         // Add shortcut text
-        newStatusBar(i18n.getString("statusBarRunning"));
+        TStatusBar statusBar = newStatusBar(i18n.getString("statusBarRunning"));
+        statusBar.addShortcutKeypress(kbF1, cmHelp,
+            i18n.getString("statusBarHelp"));
+        statusBar.addShortcutKeypress(kbF10, cmMenu,
+            i18n.getString("statusBarMenu"));
 
         // Spin it up
         terminal = new TTerminalWidget(this, 0, 0, new TAction() {
@@ -261,7 +270,10 @@ public class TTerminalWindow extends TScrollableWindow {
      */
     @Override
     public void onKeypress(final TKeypressEvent keypress) {
-        if ((terminal != null) && (terminal.isReading())) {
+        if ((terminal != null)
+            && (terminal.isReading())
+            && (!inKeyboardResize)
+        ) {
             terminal.onKeypress(keypress);
         } else {
             super.onKeypress(keypress);
@@ -330,6 +342,16 @@ public class TTerminalWindow extends TScrollableWindow {
         }
     }
 
+    /**
+     * Get this window's help topic to load.
+     *
+     * @return the topic name
+     */
+    @Override
+    public String getHelpTopic() {
+        return "Terminal Window";
+    }
+
     // ------------------------------------------------------------------------
     // TTerminalWindow --------------------------------------------------------
     // ------------------------------------------------------------------------
index ed22f492a49257944595dc19d53cbb2f3d3d9429..44a19f6809157d15e6098ad24eef59b8db913b80 100644 (file)
@@ -1,2 +1,4 @@
 windowTitle=Terminal
 statusBarRunning=Terminal session executing...
+statusBarHelp=Help
+statusBarMenu=Menu
index 22bc4b89051d31e586a60b91a274b55d3292bade..f6d7febcc0aefdcd159aa3e5af50d72f1631be0f 100644 (file)
@@ -29,7 +29,7 @@
 package jexer;
 
 import java.util.Arrays;
-import java.util.LinkedList;
+import java.util.ArrayList;
 import java.util.List;
 
 import jexer.bits.CellAttributes;
@@ -162,7 +162,7 @@ public class TText extends TScrollableWidget {
         this.text = text;
         this.colorKey = colorKey;
 
-        lines = new LinkedList<String>();
+        lines = new ArrayList<String>();
 
         vScroller = new TVScroller(this, getWidth() - 1, 0,
             Math.max(1, getHeight() - 1));
@@ -403,7 +403,7 @@ public class TText extends TScrollableWidget {
     /**
      * Set justification.
      *
-     * @param justification LEFT, CENTER, RIGHT, or FULL
+     * @param justification NONE, LEFT, CENTER, RIGHT, or FULL
      */
     public void setJustification(final Justification justification) {
         this.justification = justification;
@@ -442,4 +442,12 @@ public class TText extends TScrollableWidget {
         reflowData();
     }
 
+    /**
+     * Un-justify the text.
+     */
+    public void unJustify() {
+        justification = Justification.NONE;
+        reflowData();
+    }
+
 }
index ae3b85dd01fe8e2b0d7379836e14b833e2e792a6..4d14d0eee2debcf23b03e2df314ea41721c38c8c 100644 (file)
@@ -199,6 +199,11 @@ public class TWindow extends TWidget {
      */
     private boolean hideMouse = false;
 
+    /**
+     * The help topic for this window.
+     */
+    protected String helpTopic = "Help";
+
     // ------------------------------------------------------------------------
     // Constructors -----------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -1433,6 +1438,15 @@ public class TWindow extends TWidget {
         this.hideMouse = hideMouse;
     }
 
+    /**
+     * Get this window's help topic to load.
+     *
+     * @return the topic name
+     */
+    public String getHelpTopic() {
+        return helpTopic;
+    }
+
     /**
      * Generate a human-readable string for this window.
      *
@@ -1440,8 +1454,9 @@ public class TWindow extends TWidget {
      */
     @Override
     public String toString() {
-        return String.format("%s(%8x) \'%s\' position (%d, %d) geometry %dx%d" +
-            " hidden %s modal %s", getClass().getName(), hashCode(), title,
+        return String.format("%s(%8x) \'%s\' Z %d position (%d, %d) " +
+            "geometry %dx%d  hidden %s modal %s",
+            getClass().getName(), hashCode(), title, getZ(),
             getX(), getY(), getWidth(), getHeight(), hidden, isModal());
     }
 
index 99eaa522c880b1bfb7700e41e9aef44c25bbbb23..3efce633243c9f82433d0212ad13e6caefe4922c 100644 (file)
@@ -681,6 +681,48 @@ public class ColorTheme {
         color.setBold(false);
         colors.put("tsplitpane", color);
 
+        // THelpWindow border - during window movement
+        color = new CellAttributes();
+        color.setForeColor(Color.GREEN);
+        color.setBackColor(Color.CYAN);
+        color.setBold(true);
+        colors.put("thelpwindow.windowmove", color);
+
+        // THelpWindow border
+        color = new CellAttributes();
+        color.setForeColor(Color.GREEN);
+        color.setBackColor(Color.CYAN);
+        color.setBold(true);
+        colors.put("thelpwindow.border", color);
+
+        // THelpWindow background
+        color = new CellAttributes();
+        color.setForeColor(Color.WHITE);
+        color.setBackColor(Color.CYAN);
+        color.setBold(true);
+        colors.put("thelpwindow.background", color);
+
+        // THelpWindow text
+        color = new CellAttributes();
+        color.setForeColor(Color.WHITE);
+        color.setBackColor(Color.BLUE);
+        color.setBold(false);
+        colors.put("thelpwindow.text", color);
+
+        // THelpWindow link
+        color = new CellAttributes();
+        color.setForeColor(Color.YELLOW);
+        color.setBackColor(Color.BLUE);
+        color.setBold(true);
+        colors.put("thelpwindow.link", color);
+
+        // THelpWindow link - active
+        color = new CellAttributes();
+        color.setForeColor(Color.YELLOW);
+        color.setBackColor(Color.CYAN);
+        color.setBold(true);
+        colors.put("thelpwindow.link.active", color);
+
     }
 
     /**
index 36f7b4e9b28e79e9c6175ded59bbb697b981c44d..d33f71f4e0031710e52ed008fefa0f37f28883c4 100644 (file)
@@ -472,6 +472,10 @@ public class StringUtils {
      * @return the number of text cell columns required to display this string
      */
     public static int width(final String str) {
+        if (str == null) {
+            return 0;
+        }
+
         int n = 0;
         for (int i = 0; i < str.length();) {
             int ch = str.codePointAt(i);
diff --git a/src/jexer/help/HelpFile.java b/src/jexer/help/HelpFile.java
new file mode 100644 (file)
index 0000000..7a6f49e
--- /dev/null
@@ -0,0 +1,381 @@
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer.help;
+
+import java.io.InputStream;
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.ResourceBundle;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.ParserConfigurationException;
+import org.xml.sax.SAXException;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+/**
+ * A HelpFile is a collection of Topics with a table of contents and index of
+ * relevant terms.
+ */
+public class HelpFile {
+
+    /**
+     * Translated strings.
+     */
+    private static final ResourceBundle i18n = ResourceBundle.getBundle(HelpFile.class.getName());
+
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * The XML factory.
+     */
+    private static DocumentBuilder domBuilder;
+
+    /**
+     * The map of topics by title.
+     */
+    private HashMap<String, Topic> topicsByTitle;
+
+    /**
+     * The map of topics by index key term.
+     */
+    private HashMap<String, Topic> topicsByTerm;
+
+    /**
+     * The special "table of contents" topic.
+     */
+    private Topic tableOfContents;
+
+    /**
+     * The special "index" topic.
+     */
+    private Topic index;
+
+    /**
+     * The name of this help file.
+     */
+    private String name = "";
+
+    /**
+     * The version of this help file.
+     */
+    private String version = "";
+
+    /**
+     * The help file author.
+     */
+    private String author = "";
+
+    /**
+     * The help file copyright/written by date.
+     */
+    private String date = "";
+
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    // ------------------------------------------------------------------------
+    // HelpFile ---------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Load a help file from an input stream.
+     *
+     * @param input the input strem
+     * @throws IOException if an I/O error occurs
+     * @throws ParserConfigurationException if no XML parser is available
+     * @throws SAXException if XML parsing fails
+     */
+    public void load(final InputStream input) throws IOException,
+                                ParserConfigurationException, SAXException {
+
+        topicsByTitle = new HashMap<String, Topic>();
+        topicsByTerm = new HashMap<String, Topic>();
+
+        try {
+            loadTopics(input);
+        } finally {
+            // Always generate the TOC and Index from what was read.
+            generateTableOfContents();
+            generateIndex();
+        }
+    }
+
+    /**
+     * Get a topic by title.
+     *
+     * @param title the title for the topic
+     * @return the topic, or the "not found" topic if title is not found
+     */
+    public Topic getTopic(final String title) {
+        Topic topic = topicsByTitle.get(title);
+        if (topic == null) {
+            return Topic.NOT_FOUND;
+        }
+        return topic;
+    }
+
+    /**
+     * Get the special "search results" topic.
+     *
+     * @param searchString a regular expression search string
+     * @return an index topic containing topics with text that matches the
+     * search string
+     */
+    public Topic getSearchResults(final String searchString) {
+        List<Topic> allTopics = new ArrayList<Topic>();
+        allTopics.addAll(topicsByTitle.values());
+        Collections.sort(allTopics);
+
+        List<Topic> results = new ArrayList<Topic>();
+        Pattern pattern = Pattern.compile(searchString);
+        Pattern patternLower = Pattern.compile(searchString.toLowerCase());
+
+        for (Topic topic: allTopics) {
+            Matcher match = pattern.matcher(topic.getText().toLowerCase());
+            if (match.find()) {
+                results.add(topic);
+                continue;
+            }
+            match = pattern.matcher(topic.getTitle().toLowerCase());
+            if (match.find()) {
+                results.add(topic);
+                continue;
+            }
+            match = patternLower.matcher(topic.getText().toLowerCase());
+            if (match.find()) {
+                results.add(topic);
+                continue;
+            }
+            match = patternLower.matcher(topic.getTitle().toLowerCase());
+            if (match.find()) {
+                results.add(topic);
+                continue;
+            }
+        }
+
+        StringBuilder text = new StringBuilder();
+        int wordIndex = 0;
+        List<Link> links = new ArrayList<Link>();
+        for (Topic topic: results) {
+            text.append(topic.getTitle());
+            text.append("\n\n");
+
+            Link link = new Link(topic.getTitle(), topic.getTitle(), wordIndex);
+            wordIndex += link.getWordCount();
+            links.add(link);
+        }
+
+        return new Topic(MessageFormat.format(i18n.getString("searchResults"),
+                searchString), text.toString(), links);
+    }
+
+    /**
+     * Get the special "table of contents" topic.
+     *
+     * @return the table of contents topic
+     */
+    public Topic getTableOfContents() {
+        return tableOfContents;
+    }
+
+    /**
+     * Get the special "index" topic.
+     *
+     * @return the index topic
+     */
+    public Topic getIndex() {
+        return index;
+    }
+
+    /**
+     * Generate the table of contents topic.
+     */
+    private void generateTableOfContents() {
+        List<Topic> allTopics = new ArrayList<Topic>();
+        allTopics.addAll(topicsByTitle.values());
+        Collections.sort(allTopics);
+
+        StringBuilder text = new StringBuilder();
+        int wordIndex = 0;
+        List<Link> links = new ArrayList<Link>();
+        for (Topic topic: allTopics) {
+            text.append(topic.getTitle());
+            text.append("\n\n");
+
+            Link link = new Link(topic.getTitle(), topic.getTitle(), wordIndex);
+            wordIndex += link.getWordCount();
+            links.add(link);
+        }
+
+        tableOfContents = new Topic(i18n.getString("tableOfContents"),
+            text.toString(), links);
+    }
+
+    /**
+     * Generate the index topic.
+     */
+    private void generateIndex() {
+        List<Topic> allTopics = new ArrayList<Topic>();
+        allTopics.addAll(topicsByTitle.values());
+
+        HashMap<String, ArrayList<Topic>> allKeys;
+        allKeys = new HashMap<String, ArrayList<Topic>>();
+        for (Topic topic: allTopics) {
+            for (String key: topic.getIndexKeys()) {
+                key = key.toLowerCase();
+                ArrayList<Topic> topics = allKeys.get(key);
+                if (topics == null) {
+                    topics = new ArrayList<Topic>();
+                    allKeys.put(key, topics);
+                }
+                topics.add(topic);
+            }
+        }
+        List<String> keys = new ArrayList<String>();
+        keys.addAll(allKeys.keySet());
+        Collections.sort(keys);
+
+        StringBuilder text = new StringBuilder();
+        int wordIndex = 0;
+        List<Link> links = new ArrayList<Link>();
+
+        for (String key: keys) {
+            List<Topic> topics = allKeys.get(key);
+            assert (topics != null);
+            for (Topic topic: topics) {
+                String line = String.format("%15s %15s", key, topic.getTitle());
+                text.append(line);
+                text.append("\n\n");
+
+                wordIndex += key.split("\\s+").length;
+                Link link = new Link(topic.getTitle(), topic.getTitle(), wordIndex);
+                wordIndex += link.getWordCount();
+                links.add(link);
+            }
+        }
+
+        index = new Topic(i18n.getString("index"), text.toString(), links);
+    }
+
+    /**
+     * Load topics from a help file into the topics pool.
+     *
+     * @param input the input strem
+     * @throws IOException if an I/O error occurs
+     * @throws ParserConfigurationException if no XML parser is available
+     * @throws SAXException if XML parsing fails
+     */
+    private void loadTopics(final InputStream input) throws IOException,
+                                ParserConfigurationException, SAXException {
+
+        if (domBuilder == null) {
+            DocumentBuilderFactory dbFactory = DocumentBuilderFactory.
+                                                                newInstance();
+            domBuilder = dbFactory.newDocumentBuilder();
+        }
+        Document doc = domBuilder.parse(input);
+
+        // Get the document's root XML node
+        Node root = doc.getChildNodes().item(0);
+        NodeList level1 = root.getChildNodes();
+        for (int i = 0; i < level1.getLength(); i++) {
+            Node node = level1.item(i);
+            String name = node.getNodeName();
+            String value = node.getTextContent();
+
+            if (name.equals("name")) {
+                this.name = value;
+            }
+            if (name.equals("version")) {
+                this.version = value;
+            }
+            if (name.equals("author")) {
+                this.author = value;
+            }
+            if (name.equals("date")) {
+                this.date = value;
+            }
+            if (name.equals("topics")) {
+                NodeList topics = node.getChildNodes();
+                for (int j = 0; j < topics.getLength(); j++) {
+                    Node topic = topics.item(j);
+                    addTopic(topic);
+                }
+            }
+        }
+    }
+
+    /**
+     * Add a topic to this help file.
+     *
+     * @param xmlNode the topic XML node
+     * @throws IOException if a java.io operation throws
+     */
+    private void addTopic(final Node xmlNode) throws IOException {
+        String title = "";
+        String text = "";
+
+        NamedNodeMap attributes = xmlNode.getAttributes();
+        if (attributes != null) {
+            for (int i = 0; i < attributes.getLength(); i++) {
+                Node attr = attributes.item(i);
+                if (attr.getNodeName().equals("title")) {
+                    title = attr.getNodeValue().trim();
+                }
+            }
+        }
+        NodeList level2 = xmlNode.getChildNodes();
+        for (int i = 0; i < level2.getLength(); i++) {
+            Node node = level2.item(i);
+            String nodeName = node.getNodeName();
+            String nodeValue = node.getTextContent();
+            if (nodeName.equals("text")) {
+                text = nodeValue.trim();
+            }
+        }
+        if (title.length() > 0) {
+            Topic topic = new Topic(title, text);
+            topicsByTitle.put(title, topic);
+        }
+    }
+
+}
diff --git a/src/jexer/help/HelpFile.properties b/src/jexer/help/HelpFile.properties
new file mode 100644 (file)
index 0000000..803961b
--- /dev/null
@@ -0,0 +1,3 @@
+tableOfContents=Table Of Contents
+index=Index
+searchResults=Search Results - {0}
diff --git a/src/jexer/help/Link.java b/src/jexer/help/Link.java
new file mode 100644 (file)
index 0000000..665381c
--- /dev/null
@@ -0,0 +1,135 @@
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer.help;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.ResourceBundle;
+
+/**
+ * A Link is a section of text with a reference to a Topic.
+ */
+public class Link {
+
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * The topic id that this link points to.
+     */
+    private String topic;
+
+    /**
+     * The text inside the link tag.
+     */
+    private String text;
+
+    /**
+     * The number of words in this link.
+     */
+    private int wordCount;
+
+    /**
+     * The word number (from the beginning of topic text) that corresponds to
+     * the first word of this link.
+     */
+    private int index;
+
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Public constructor.
+     *
+     * @param topic the topic to point to
+     * @param text the text inside the link tag
+     * @param index the word count index
+     */
+    public Link(final String topic, final String text, final int index) {
+        this.topic = topic;
+        this.text = text;
+        this.index = index;
+        this.wordCount = text.split("\\s+").length;
+    }
+
+    // ------------------------------------------------------------------------
+    // Link -------------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Get the topic.
+     *
+     * @return the topic
+     */
+    public String getTopic() {
+        return topic;
+    }
+
+    /**
+     * Get the link text.
+     *
+     * @return the text inside the link tag
+     */
+    public String getText() {
+        return text;
+    }
+
+    /**
+     * Get the word index for this link.
+     *
+     * @return the word number (from the beginning of topic text) that
+     * corresponds to the first word of this link
+     */
+    public int getIndex() {
+        return index;
+    }
+
+    /**
+     * Get the number of words in this link.
+     *
+     * @return the number of words in this link
+     */
+    public int getWordCount() {
+        return wordCount;
+    }
+
+    /**
+     * Generate a human-readable string for this widget.
+     *
+     * @return a human-readable string
+     */
+    @Override
+    public String toString() {
+        return String.format("%s(%8x) topic %s link text %s word # %d count %d",
+            getClass().getName(), hashCode(), topic, text, index, wordCount);
+    }
+
+}
diff --git a/src/jexer/help/THelpText.java b/src/jexer/help/THelpText.java
new file mode 100644 (file)
index 0000000..2e0afcf
--- /dev/null
@@ -0,0 +1,389 @@
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer.help;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import jexer.THelpWindow;
+import jexer.TScrollableWidget;
+import jexer.TVScroller;
+import jexer.TWidget;
+import jexer.bits.CellAttributes;
+import jexer.bits.StringUtils;
+import jexer.event.TKeypressEvent;
+import jexer.event.TMouseEvent;
+import static jexer.TKeypress.*;
+
+/**
+ * THelpText displays help text with clickable links in a scrollable text
+ * area. It reflows automatically on resize.
+ */
+public class THelpText extends TScrollableWidget {
+
+    // ------------------------------------------------------------------------
+    // Constants --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * The number of lines to scroll on mouse wheel up/down.
+     */
+    private static final int wheelScrollSize = 3;
+
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * The paragraphs in this text box.
+     */
+    private List<TParagraph> paragraphs;
+
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Public constructor.
+     *
+     * @param parent parent widget
+     * @param topic the topic to display
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param width width of text area
+     * @param height height of text area
+     */
+    public THelpText(final THelpWindow parent, final Topic topic, final int x,
+        final int y, final int width, final int height) {
+
+        // Set parent and window
+        super(parent, x, y, width, height);
+
+        vScroller = new TVScroller(this, getWidth() - 1, 0,
+            Math.max(1, getHeight()));
+
+        setTopic(topic);
+    }
+
+    // ------------------------------------------------------------------------
+    // TScrollableWidget ------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Override TWidget's width: we need to set child widget widths.
+     *
+     * @param width new widget width
+     */
+    @Override
+    public void setWidth(final int width) {
+        super.setWidth(width);
+        if (hScroller != null) {
+            hScroller.setWidth(getWidth() - 1);
+        }
+        if (vScroller != null) {
+            vScroller.setX(getWidth() - 1);
+        }
+    }
+
+    /**
+     * Override TWidget's height: we need to set child widget heights.
+     * time.
+     *
+     * @param height new widget height
+     */
+    @Override
+    public void setHeight(final int height) {
+        super.setHeight(height);
+        if (hScroller != null) {
+            hScroller.setY(getHeight() - 1);
+        }
+        if (vScroller != null) {
+            vScroller.setHeight(Math.max(1, getHeight()));
+        }
+    }
+
+    /**
+     * Handle mouse press events.
+     *
+     * @param mouse mouse button press event
+     */
+    @Override
+    public void onMouseDown(final TMouseEvent mouse) {
+        // Pass to children
+        super.onMouseDown(mouse);
+
+        if (mouse.isMouseWheelUp()) {
+            for (int i = 0; i < wheelScrollSize; i++) {
+                vScroller.decrement();
+            }
+            reflowData();
+            return;
+        }
+        if (mouse.isMouseWheelDown()) {
+            for (int i = 0; i < wheelScrollSize; i++) {
+                vScroller.increment();
+            }
+            reflowData();
+            return;
+        }
+
+        // User clicked on a paragraph, update the scrollbar accordingly.
+        for (int i = 0; i < paragraphs.size(); i++) {
+            if (paragraphs.get(i).isActive()) {
+                setVerticalValue(i);
+                return;
+            }
+        }
+    }
+
+    /**
+     * Handle keystrokes.
+     *
+     * @param keypress keystroke event
+     */
+    @Override
+    public void onKeypress(final TKeypressEvent keypress) {
+        if (keypress.equals(kbTab)) {
+            getParent().switchWidget(true);
+        } else if (keypress.equals(kbShiftTab)) {
+            getParent().switchWidget(false);
+        } else if (keypress.equals(kbUp)) {
+            if (!paragraphs.get(getVerticalValue()).up()) {
+                vScroller.decrement();
+                reflowData();
+            }
+        } else if (keypress.equals(kbDown)) {
+            if (!paragraphs.get(getVerticalValue()).down()) {
+                vScroller.increment();
+                reflowData();
+            }
+        } else if (keypress.equals(kbPgUp)) {
+            vScroller.bigDecrement();
+            reflowData();
+        } else if (keypress.equals(kbPgDn)) {
+            vScroller.bigIncrement();
+            reflowData();
+        } else if (keypress.equals(kbHome)) {
+            vScroller.toTop();
+            reflowData();
+        } else if (keypress.equals(kbEnd)) {
+            vScroller.toBottom();
+            reflowData();
+        } else {
+            // Pass other keys on
+            super.onKeypress(keypress);
+        }
+    }
+
+    /**
+     * Place the scrollbars on the edge of this widget, and adjust bigChange
+     * to match the new size.  This is called by onResize().
+     */
+    protected void placeScrollbars() {
+        if (hScroller != null) {
+            hScroller.setY(getHeight() - 1);
+            hScroller.setWidth(getWidth() - 1);
+            hScroller.setBigChange(getWidth() - 1);
+        }
+        if (vScroller != null) {
+            vScroller.setX(getWidth() - 1);
+            vScroller.setHeight(getHeight());
+            vScroller.setBigChange(getHeight());
+        }
+    }
+
+    /**
+     * Resize text and scrollbars for a new width/height.
+     */
+    @Override
+    public void reflowData() {
+        for (TParagraph paragraph: paragraphs) {
+            paragraph.setWidth(getWidth() - 1);
+            paragraph.reflowData();
+        }
+
+        int top = getVerticalValue();
+        int paragraphsHeight = 0;
+        for (TParagraph paragraph: paragraphs) {
+            paragraphsHeight += paragraph.getHeight();
+        }
+        if (paragraphsHeight <= getHeight()) {
+            // All paragraphs fit in the window.
+            int y = 0;
+            for (int i = 0; i < paragraphs.size(); i++) {
+                paragraphs.get(i).setEnabled(true);
+                paragraphs.get(i).setVisible(true);
+                paragraphs.get(i).setY(y);
+                y += paragraphs.get(i).getHeight();
+            }
+            activate(paragraphs.get(getVerticalValue()));
+            return;
+        }
+
+        /*
+         * Some paragraphs will not fit in the window.  Find the number of
+         * rows needed to display from the current vertical position to the
+         * end:
+         *
+         * - If this meets or exceeds the available height, then draw from
+         *   the vertical position to the number of visible rows.
+         *
+         * - If this is less than the available height, back up until
+         *   meeting/exceeding the height, and draw from there to the end.
+         *
+         */
+        int rowsNeeded = 0;
+        for (int i = getVerticalValue(); i <= getBottomValue(); i++) {
+            rowsNeeded += paragraphs.get(i).getHeight();
+        }
+        while (rowsNeeded < getHeight()) {
+            // Decrease top until we meet/exceed the visible display.
+            if (top == getTopValue()) {
+                break;
+            }
+            top--;
+            rowsNeeded += paragraphs.get(top).getHeight();
+        }
+
+        // All set, now disable all paragraphs except the visible ones.
+        for (TParagraph paragraph: paragraphs) {
+            paragraph.setEnabled(false);
+            paragraph.setVisible(false);
+            paragraph.setY(-1);
+        }
+        int y = 0;
+        for (int i = top; (i <= getBottomValue()) && (y < getHeight()); i++) {
+            paragraphs.get(i).setEnabled(true);
+            paragraphs.get(i).setVisible(true);
+            paragraphs.get(i).setY(y);
+            y += paragraphs.get(i).getHeight();
+        }
+        activate(paragraphs.get(getVerticalValue()));
+    }
+
+    /**
+     * Draw the text box.
+     */
+    @Override
+    public void draw() {
+        // Setup my color
+        CellAttributes color = getTheme().getColor("thelpwindow.text");
+        for (int y = 0; y < getHeight(); y++) {
+            hLineXY(0, y, getWidth(), ' ', color);
+        }
+    }
+
+    // ------------------------------------------------------------------------
+    // THelpText --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Set the topic.
+     *
+     * @param topic new topic to display
+     */
+    public void setTopic(final Topic topic) {
+        setTopic(topic, true);
+    }
+
+    /**
+     * Set the topic.
+     *
+     * @param topic new topic to display
+     * @param separator if true, separate paragraphs
+     */
+    public void setTopic(final Topic topic, final boolean separator) {
+
+        if (paragraphs != null) {
+            getChildren().removeAll(paragraphs);
+        }
+        paragraphs = new ArrayList<TParagraph>();
+
+        // Add title paragraph at top.  We explicitly set the separator to
+        // false to achieve the underscore effect.
+        List<TWord> title = new ArrayList<TWord>();
+        title.add(new TWord(topic.getTitle(), null));
+        TParagraph titleParagraph = new TParagraph(this, title);
+        titleParagraph.separator = false;
+        paragraphs.add(titleParagraph);
+        title = new ArrayList<TWord>();
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < topic.getTitle().length(); i++) {
+            sb.append('\u2580');
+        }
+        title.add(new TWord(sb.toString(), null));
+        titleParagraph = new TParagraph(this, title);
+        paragraphs.add(titleParagraph);
+
+        // Now add the actual text as paragraphs.
+        int wordIndex = 0;
+
+        // Break up text into paragraphs
+        String [] blocks = topic.getText().split("\n\n");
+        for (String block: blocks) {
+            List<TWord> words = new ArrayList<TWord>();
+            String [] lines = block.split("\n");
+            for (String line: lines) {
+                line = line.trim();
+                // System.err.println("line: " + line);
+                String [] wordTokens = line.split("\\s+");
+                for (int i = 0; i < wordTokens.length; i++) {
+                    String wordStr = wordTokens[i].trim();
+                    Link wordLink = null;
+                    for (Link link: topic.getLinks()) {
+                        if ((i + wordIndex >= link.getIndex())
+                            && (i + wordIndex < link.getIndex() + link.getWordCount())
+                        ) {
+                            // This word is part of a link.
+                            wordLink = link;
+                            wordStr = link.getText();
+                            i += link.getWordCount() - 1;
+                            break;
+                        }
+                    }
+                    TWord word = new TWord(wordStr, wordLink);
+                    /*
+                    System.err.println("add word at " + (i + wordIndex) + " : "
+                        + wordStr + " " + wordLink);
+                     */
+                    words.add(word);
+                } // for (int i = 0; i < words.length; i++)
+                wordIndex += wordTokens.length;
+            } // for (String line: lines)
+            TParagraph paragraph = new TParagraph(this, words);
+            paragraph.separator = separator;
+            paragraphs.add(paragraph);
+        } // for (String block: blocks)
+
+        setBottomValue(paragraphs.size() - 1);
+        setVerticalValue(0);
+        reflowData();
+    }
+
+}
diff --git a/src/jexer/help/TParagraph.java b/src/jexer/help/TParagraph.java
new file mode 100644 (file)
index 0000000..04559da
--- /dev/null
@@ -0,0 +1,175 @@
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer.help;
+
+import java.util.List;
+
+import jexer.TWidget;
+
+/**
+ * TParagraph contains a reflowable collection of TWords, some of which are
+ * clickable links.
+ */
+public class TParagraph extends TWidget {
+
+    // ------------------------------------------------------------------------
+    // Constants --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Topic text and links converted to words.
+     */
+    private List<TWord> words;
+
+    /**
+     * If true, add one row to height as a paragraph separator.  Note package
+     * private access.
+     */
+    boolean separator = true;
+
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Public constructor.
+     *
+     * @param parent parent widget
+     * @param words the pieces of the paragraph to display
+     */
+    public TParagraph(final THelpText parent, final List<TWord> words) {
+
+        // Set parent and window
+        super(parent, 0, 0, parent.getWidth() - 1, 1);
+
+        this.words = words;
+        for (TWord word: words) {
+            word.setParent(this, false);
+        }
+
+        reflowData();
+    }
+
+    // ------------------------------------------------------------------------
+    // TWidget ----------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    // ------------------------------------------------------------------------
+    // TParagraph -------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Reposition the words in this paragraph to reflect the new width, and
+     * set the paragraph height.
+     */
+    public void reflowData() {
+        int x = 0;
+        int y = 0;
+        for (TWord word: words) {
+            if (x + word.getWidth() >= getWidth()) {
+                x = 0;
+                y++;
+            }
+            word.setX(x);
+            word.setY(y);
+            x += word.getWidth() + 1;
+        }
+        if (separator) {
+            setHeight(y + 2);
+        } else {
+            setHeight(y + 1);
+        }
+    }
+
+    /**
+     * Try to select a previous link.
+     *
+     * @return true if there was a previous link in this paragraph to select
+     */
+    public boolean up() {
+        if (words.size() == 0) {
+            return false;
+        }
+        if (getActiveChild() == this) {
+            // No selectable links
+            return false;
+        }
+        TWord firstWord = null;
+        TWord lastWord = null;
+        for (TWord word: words) {
+            if (word.isEnabled()) {
+                if (firstWord == null) {
+                    firstWord = word;
+                }
+                lastWord = word;
+            }
+        }
+        if (getActiveChild() == firstWord) {
+            return false;
+        }
+        switchWidget(false);
+        return true;
+    }
+
+    /**
+     * Try to select a next link.
+     *
+     * @return true if there was a next link in this paragraph to select
+     */
+    public boolean down() {
+        if (words.size() == 0) {
+            return false;
+        }
+        if (getActiveChild() == this) {
+            // No selectable links
+            return false;
+        }
+        TWord firstWord = null;
+        TWord lastWord = null;
+        for (TWord word: words) {
+            if (word.isEnabled()) {
+                if (firstWord == null) {
+                    firstWord = word;
+                }
+                lastWord = word;
+            }
+        }
+        if (getActiveChild() == lastWord) {
+            return false;
+        }
+        switchWidget(true);
+        return true;
+    }
+
+
+}
diff --git a/src/jexer/help/TWord.java b/src/jexer/help/TWord.java
new file mode 100644 (file)
index 0000000..d46a22e
--- /dev/null
@@ -0,0 +1,143 @@
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer.help;
+
+import jexer.THelpWindow;
+import jexer.TWidget;
+import jexer.bits.CellAttributes;
+import jexer.bits.StringUtils;
+import jexer.event.TKeypressEvent;
+import jexer.event.TMouseEvent;
+import static jexer.TKeypress.*;
+
+/**
+ * TWord contains either a string to display or a clickable link.
+ */
+public class TWord extends TWidget {
+
+    // ------------------------------------------------------------------------
+    // Constants --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * The word(s) to display.
+     */
+    private String words;
+
+    /**
+     * Link to another Topic.
+     */
+    private Link link;
+
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Public constructor.
+     *
+     * @param words the words to display
+     * @param link link to other topic, or null
+     */
+    public TWord(final String words, final Link link) {
+
+        // TWord is created by THelpText before the TParagraph is belongs to
+        // is created, so pass null as parent for now.
+        super(null, 0, 0, StringUtils.width(words), 1);
+
+        this.words = words;
+        this.link = link;
+
+        // Don't make text-only words "active".
+        if (link == null) {
+            setEnabled(false);
+        }
+    }
+
+    // ------------------------------------------------------------------------
+    // Event handlers ---------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Handle mouse press events.
+     *
+     * @param mouse mouse button press event
+     */
+    @Override
+    public void onMouseDown(final TMouseEvent mouse) {
+        if (mouse.isMouse1()) {
+            if (link != null) {
+                ((THelpWindow) getWindow()).setHelpTopic(link.getTopic());
+            }
+        }
+    }
+
+    /**
+     * Handle keystrokes.
+     *
+     * @param keypress keystroke event
+     */
+    @Override
+    public void onKeypress(final TKeypressEvent keypress) {
+        if (keypress.equals(kbEnter)) {
+            if (link != null) {
+                ((THelpWindow) getWindow()).setHelpTopic(link.getTopic());
+            }
+        }
+    }
+
+    // ------------------------------------------------------------------------
+    // TWidget ----------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Draw the words.
+     */
+    @Override
+    public void draw() {
+        CellAttributes color = getTheme().getColor("thelpwindow.text");
+        if (link != null) {
+            if (isAbsoluteActive()) {
+                color = getTheme().getColor("thelpwindow.link.active");
+            } else {
+                color = getTheme().getColor("thelpwindow.link");
+            }
+        }
+        putStringXY(0, 0, words, color);
+    }
+
+    // ------------------------------------------------------------------------
+    // TWord ------------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+}
diff --git a/src/jexer/help/Topic.java b/src/jexer/help/Topic.java
new file mode 100644 (file)
index 0000000..8c0bc71
--- /dev/null
@@ -0,0 +1,339 @@
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer.help;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.ResourceBundle;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A Topic is a page of help text with a title and possibly links to other
+ * Topics.
+ */
+public class Topic implements Comparable<Topic> {
+
+    /**
+     * Translated strings.
+     */
+    private static final ResourceBundle i18n = ResourceBundle.getBundle(Topic.class.getName());
+
+    // ------------------------------------------------------------------------
+    // Constants --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * The "not found" topic to display when a key or index term does not
+     * have an associated topic.  Note package private access.
+     */
+    static Topic NOT_FOUND = null;
+
+    /**
+     * The regex for identifying index tags.
+     */
+    private static final String INDEX_REGEX_STR = "\\#\\{([^\\}]*)\\}";
+
+    /**
+     * The regex for identifying link tags.
+     */
+    private static final String LINK_REGEX_STR = "\\[([^\\]]*)\\]\\(([^\\)]*)\\)";
+
+    /**
+     * The regex for identifying words.
+     */
+    private static final String WORD_REGEX_STR = "[ \\t]+";
+
+    /**
+     * The index match regex.
+     */
+    private static Pattern INDEX_REGEX;
+
+    /**
+     * The link match regex.
+     */
+    private static Pattern LINK_REGEX;
+
+    /**
+     * The word match regex.
+     */
+    private static Pattern WORD_REGEX;
+
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * The title for this topic.
+     */
+    private String title;
+
+    /**
+     * The text for this topic.
+     */
+    private String text;
+
+    /**
+     * The index keys in this topic.
+     */
+    private Set<String> indexKeys = new HashSet<String>();
+
+    /**
+     * The links in this topic.
+     */
+    private List<Link> links = new ArrayList<Link>();
+
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Static constructor.
+     */
+    static {
+        try {
+            INDEX_REGEX = Pattern.compile(INDEX_REGEX_STR);
+            LINK_REGEX = Pattern.compile(LINK_REGEX_STR);
+            WORD_REGEX = Pattern.compile(WORD_REGEX_STR);
+
+            NOT_FOUND = new Topic(i18n.getString("topicNotFoundTitle"),
+                i18n.getString("topicNotFoundText"));
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * Public constructor.
+     *
+     * @param title the topic title
+     * @param text the topic text
+     */
+    public Topic(final String title, final String text) {
+        this.title = title;
+        processText(text);
+    }
+
+    /**
+     * Package private constructor.
+     *
+     * @param title the topic title
+     * @param text the topic text
+     * @param links links to add after processing text
+     */
+    Topic(final String title, final String text, final List<Link> links) {
+        this.title = title;
+        processText(text);
+        this.links.addAll(links);
+    }
+
+    // ------------------------------------------------------------------------
+    // Topic ------------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Get the topic title.
+     *
+     * @return the title
+     */
+    public String getTitle() {
+        return title;
+    }
+
+    /**
+     * Get the topic text.
+     *
+     * @return the text
+     */
+    public String getText() {
+        return text;
+    }
+
+    /**
+     * Get the index keys.
+     *
+     * @return the keys
+     */
+    public Set<String> getIndexKeys() {
+        return indexKeys;
+    }
+
+    /**
+     * Get the links.
+     *
+     * @return the links
+     */
+    public List<Link> getLinks() {
+        return links;
+    }
+
+    /**
+     * Comparison operator.
+     *
+     * @param that another Topic instance
+     * @return comparison by topic title
+     */
+    public int compareTo(final Topic that) {
+        return title.compareTo(that.title);
+    }
+
+    /**
+     * Generate a human-readable string for this widget.
+     *
+     * @return a human-readable string
+     */
+    @Override
+    public String toString() {
+        return String.format("%s(%8x) topic %s text %s links %s indexKeys %s",
+            getClass().getName(), hashCode(), title, text, links, indexKeys);
+    }
+
+    /**
+     * Process a string through the regexes, building up the indexes and
+     * links.
+     *
+     * @param text the text to process
+     */
+    private void processText(final String text) {
+        StringBuilder sb = new StringBuilder();
+        String [] lines = text.split("\n");
+        int wordIndex = 0;
+        for (String line: lines) {
+            line = line.trim();
+
+            String cleanLine = "";
+
+            // System.err.println("LINE " + wordIndex + " : '" + line + "'");
+
+            Matcher index = INDEX_REGEX.matcher(line);
+            int start = 0;
+            while (index.find()) {
+                cleanLine += line.substring(start, index.start());
+                String key = index.group(1);
+                cleanLine += key;
+                start = index.end();
+                // System.err.println("ADD KEY: " + key);
+                indexKeys.add(key);
+            }
+            cleanLine += line.substring(start);
+
+            line = cleanLine;
+            cleanLine = "";
+
+            /*
+            System.err.println("line after removing #{index} tags: " +
+                wordIndex + " '" + line + "'");
+            */
+
+            Matcher link = LINK_REGEX.matcher(line);
+            start = 0;
+
+            boolean hasLink = link.find();
+
+            // System.err.println("hasLink " + hasLink);
+
+            while (true) {
+
+                if (hasLink == false) {
+                    cleanLine += line.substring(start);
+
+                    String remaining = line.substring(start).trim();
+                    Matcher word = WORD_REGEX.matcher(remaining);
+                    while (word.find()) {
+                        // System.err.println("word.find() true");
+                        wordIndex++;
+                    }
+                    if (remaining.length() > 0) {
+                        // The last word on the line.
+                        wordIndex++;
+                    }
+                    break;
+                }
+
+                assert (hasLink == true);
+
+                int linkWordIndex = link.start();
+                int cleanLineStart = cleanLine.length();
+                cleanLine += line.substring(start, linkWordIndex);
+                String linkText = link.group(1);
+                String topic = link.group(2);
+                cleanLine += linkText;
+                start = link.end();
+
+                // Increment wordIndex until we reach the first word of
+                // the link text.
+                Matcher word = WORD_REGEX.matcher(cleanLine.
+                    substring(cleanLineStart));
+                while (word.find()) {
+                    if (word.end() <= linkWordIndex) {
+                        wordIndex++;
+                    } else {
+                        // We have found the word that matches the first
+                        // word of link text, bail out.
+                        break;
+                    }
+                }
+                /*
+                System.err.println("ADD LINK --> " + topic + ": '" +
+                    linkText + "' word index " + wordIndex);
+                */
+                links.add(new Link(topic, linkText, wordIndex));
+
+                // The rest of the words in the link text.
+                while (word.find()) {
+                    wordIndex++;
+                }
+                // The final word after the last whitespace.
+                wordIndex++;
+
+                hasLink = link.find();
+                if (hasLink) {
+                    wordIndex += 3;
+                }
+            }
+
+
+            /*
+            System.err.println("line after removing [link](...) tags: '" +
+                cleanLine + "'");
+            */
+
+            // Append the entire line.
+            sb.append(cleanLine);
+            sb.append("\n");
+
+            this.text = sb.toString();
+
+        } // for (String line: lines)
+
+    }
+
+}
diff --git a/src/jexer/help/Topic.properties b/src/jexer/help/Topic.properties
new file mode 100644 (file)
index 0000000..1c8de6f
--- /dev/null
@@ -0,0 +1,2 @@
+topicNotFoundTitle=Topic Not Found
+topicNotFoundText=The help topic was not found.
diff --git a/src/jexer/help/package-info.java b/src/jexer/help/package-info.java
new file mode 100644 (file)
index 0000000..409c370
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+
+/**
+ * Online help system.
+ */
+package jexer.help;