--- /dev/null
+<?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>
package jexer;
import java.io.File;
+import java.io.FileInputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
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;
*/
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
}
}
+ // 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);
+ }
}
// ------------------------------------------------------------------------
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;
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;
aboutDialogTitle=About
aboutDialogText=Jexer Version {0}
+
+searchHelpInputBoxTitle=Search Help Topics
+searchHelpInputBoxCaption=Search help topics for (regex):
// 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)
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}
--- /dev/null
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer;
+
+import java.util.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);
+ }
+
+}
--- /dev/null
+windowTitle=Help
+previousButton=Pre&vious
+contentsButton=Co&ntents
+indexButton=\ &Index\ \
+closeButton=\ C&lose\ \
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 {
import jexer.event.TMenuEvent;
import jexer.event.TMouseEvent;
import jexer.event.TResizeEvent;
+import static jexer.TCommand.*;
import static jexer.TKeypress.*;
/**
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() {
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() {
*/
@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);
}
}
+ /**
+ * Get this window's help topic to load.
+ *
+ * @return the topic name
+ */
+ @Override
+ public String getHelpTopic() {
+ return "Terminal Window";
+ }
+
// ------------------------------------------------------------------------
// TTerminalWindow --------------------------------------------------------
// ------------------------------------------------------------------------
windowTitle=Terminal
statusBarRunning=Terminal session executing...
+statusBarHelp=Help
+statusBarMenu=Menu
package jexer;
import java.util.Arrays;
-import java.util.LinkedList;
+import java.util.ArrayList;
import java.util.List;
import jexer.bits.CellAttributes;
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));
/**
* 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;
reflowData();
}
+ /**
+ * Un-justify the text.
+ */
+ public void unJustify() {
+ justification = Justification.NONE;
+ reflowData();
+ }
+
}
*/
private boolean hideMouse = false;
+ /**
+ * The help topic for this window.
+ */
+ protected String helpTopic = "Help";
+
// ------------------------------------------------------------------------
// Constructors -----------------------------------------------------------
// ------------------------------------------------------------------------
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.
*
*/
@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());
}
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);
+
}
/**
* @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);
--- /dev/null
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer.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);
+ }
+ }
+
+}
--- /dev/null
+tableOfContents=Table Of Contents
+index=Index
+searchResults=Search Results - {0}
--- /dev/null
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer.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);
+ }
+
+}
--- /dev/null
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer.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();
+ }
+
+}
--- /dev/null
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer.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;
+ }
+
+
+}
--- /dev/null
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer.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 ------------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+}
--- /dev/null
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer.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)
+
+ }
+
+}
--- /dev/null
+topicNotFoundTitle=Topic Not Found
+topicNotFoundText=The help topic was not found.
--- /dev/null
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+
+/**
+ * Online help system.
+ */
+package jexer.help;