help system
[fanfix.git] / src / jexer / help / THelpText.java
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();
+    }
+
+}