reflowable text box
authorKevin Lamonte <kevin.lamonte@gmail.com>
Sun, 15 Mar 2015 15:01:26 +0000 (11:01 -0400)
committerKevin Lamonte <kevin.lamonte@gmail.com>
Sun, 15 Mar 2015 15:01:26 +0000 (11:01 -0400)
Makefile
README.md
demos/Demo1.java
src/jexer/THScroller.java
src/jexer/TText.java [new file with mode: 0644]
src/jexer/TVScroller.java
src/jexer/TWidget.java

index 0ba0eb4b3d61228c90d0cb576211b32694591afd..63c74ec14adc47888910f1cfc3c4036962f4bf13 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -44,6 +44,7 @@ JEXER_SRC = $(SRC_DIR)/jexer/TApplication.java \
        $(SRC_DIR)/jexer/TLabel.java \
        $(SRC_DIR)/jexer/TCommand.java \
        $(SRC_DIR)/jexer/TKeypress.java \
+       $(SRC_DIR)/jexer/TText.java \
        $(SRC_DIR)/jexer/THScroller.java \
        $(SRC_DIR)/jexer/TVScroller.java \
        $(SRC_DIR)/jexer/TWidget.java \
@@ -78,6 +79,7 @@ JEXER_BIN = $(TARGET_DIR)/jexer/TApplication.class \
        $(TARGET_DIR)/jexer/TLabel.class \
        $(TARGET_DIR)/jexer/TCommand.class \
        $(TARGET_DIR)/jexer/TKeypress.class \
+       $(TARGET_DIR)/jexer/TText.class \
        $(TARGET_DIR)/jexer/THScroller.class \
        $(TARGET_DIR)/jexer/TVScroller.class \
        $(TARGET_DIR)/jexer/TWidget.class \
index bbe38d640af88567e030fe45f93132870b34ad8c..48d1bfffb8a773c119af64852527be59159a21df 100644 (file)
--- a/README.md
+++ b/README.md
@@ -58,23 +58,29 @@ Many tasks remain before calling this version 1.0:
 
 0.0.1:
 
-- TDirectoryList
 - TMessageBox
-- THScroller / TVScroller
-- TText
-- TTreeView
+- AWTBackend
 
 0.0.2:
 
-- TEditor
+- TTreeView
+- TDirectoryList
 - TFileOpen
-- TTerminal
 
 0.0.3:
 
+- TEditor
+- TTerminal
+
+0.0.4:
+
 - Bugs
+  - TTimer is jittery with I/O
   - TSubMenu keyboard mnemonic not working
   - kbDel assertion failure in TMenu (MID_CLEAR)
+  - TDirectoryList cannot be navigated only with keyboard
+  - TTreeView cannot be navigated only with keyboard
+  - RangeViolation after dragging scrollbar up/down
 - TEditor
   - Word wrap
   - Forward/backward word
@@ -82,22 +88,18 @@ Many tasks remain before calling this version 1.0:
   - Replace
   - Cut/Copy/Paste
 
-0.0.4:
+0.1.0:
 
+- TWindow
+  - "Smart placement" for new windows
 - ECMATerminal
   - Mouse 1006 mode parsing
-- Bugs
-  - TDirectoryList cannot be navigated only with keyboard
-  - TTreeView cannot be navigated only with keyboard
-  - RangeViolation after dragging scrollbar up/down
 
 Wishlist features (2.0):
 
 - TTerminal
   - Handle resize events (pass to child process)
   - xterm mouse handling
-- TWindow
-  - "Smart placement" for new windows
 - Screen
   - Allow complex characters in putCharXY() and detect them in putStrXY().
 - TComboBox
@@ -111,4 +113,3 @@ Wishlist features (2.0):
   - TText
   - TTerminal
   - TComboBox
-- AWTBackend
index 7735b055b80062cdb5d7a79c98b376315029c271..a179b0746eed115b90c9b79176982dcc259ba574 100644 (file)
  */
 
 import jexer.*;
+import jexer.event.*;
 import jexer.menu.*;
 
+class DemoTextWindow extends TWindow {
+
+    /**
+     * Hang onto my TText so I can resize it with the window.
+     */
+    private TText textField;
+
+    /**
+     * Public constructor.
+     */
+    public DemoTextWindow(TApplication parent) {
+        super(parent, "Text Areas", 0, 0, 44, 20, RESIZABLE);
+
+        textField = addText(
+"This is an example of a reflowable text field.  Some example text follows.\n" +
+"\n" +
+"This library implements a text-based windowing system loosely\n" +
+"reminiscient of Borland's [Turbo\n" +
+"Vision](http://en.wikipedia.org/wiki/Turbo_Vision) library.  For those\n" +
+"wishing to use the actual C++ Turbo Vision library, see [Sergio\n" +
+"Sigala's updated version](http://tvision.sourceforge.net/) that runs\n" +
+"on many more platforms.\n" +
+"\n" +
+"Currently the only console platform supported is Posix (tested on\n" +
+"Linux).  Input/output is handled through terminal escape sequences\n" +
+"generated by the library itself: ncurses is not required or linked to. \n" +
+"xterm mouse tracking using UTF8 coordinates is supported.\n" +
+"\n" +
+"This library is licensed LGPL (\"GNU Lesser General Public License\")\n" +
+"version 3 or greater.  See the file COPYING for the full license text,\n" +
+"which includes both the GPL v3 and the LGPL supplemental terms.\n" +
+"\n",
+            1, 1, 40, 16);
+    }
+
+    /**
+     * Handle window/screen resize events.
+     *
+     * @param event resize event
+     */
+    @Override
+    public void onResize(final TResizeEvent event) {
+        if (event.getType() == TResizeEvent.Type.WIDGET) {
+            // Resize the text field
+            textField.setWidth(event.getWidth() - 4);
+            textField.setHeight(event.getHeight() - 4);
+            textField.reflow();
+            return;
+        }
+
+        // Pass to children instead
+        for (TWidget widget: getChildren()) {
+            widget.onResize(event);
+        }
+    }
+}
+
 class DemoCheckboxWindow extends TWindow {
 
     /**
-     * Constructor
+     * Constructor.
      */
     DemoCheckboxWindow(TApplication parent) {
         this(parent, CENTERED | RESIZABLE);
     }
 
     /**
-     * Constructor
+     * Constructor.
      */
     DemoCheckboxWindow(TApplication parent, int flags) {
         // Construct a demo window.  X and Y don't matter because it
@@ -197,10 +255,11 @@ EOS",
 
 
 class DemoMainWindow extends TWindow {
-    // Timer that increments a number
+
+    // Timer that increments a number.
     private TTimer timer;
 
-    // Timer label is updated with timerrr ticks
+    // Timer label is updated with timer ticks.
     TLabel timerLabel;
 
     /*
@@ -304,17 +363,21 @@ class DemoMainWindow extends TWindow {
             );
         }
         row += 2;
+         */
 
         if (!isModal()) {
             addLabel("Text areas", 1, row);
             addButton("&Text", 35, row,
-                {
-                    new DemoTextWindow(application);
+                new TAction() {
+                    public void DO() {
+                        new DemoTextWindow(getApplication());
+                    }
                 }
             );
         }
         row += 2;
 
+        /*
         if (!isModal()) {
             addLabel("Tree views", 1, row);
             addButton("Tree&View", 35, row,
@@ -360,6 +423,7 @@ class DemoMainWindow extends TWindow {
  * The demo application itself.
  */
 class DemoApplication extends TApplication {
+
     /**
      * Public constructor
      */
@@ -405,9 +469,9 @@ public class Demo1 {
     /**
      * Main entry point.
      *
-     * @param  args Command line arguments
+     * @param args Command line arguments
      */
-    public static void main(String [] args) {
+    public static void main(final String [] args) {
         try {
             DemoApplication app = new DemoApplication();
             app.run();
index 62f6075a5de202a400ab045df125b0df97113d30..a3599a9c25e09071b0217b1f86cb11f1aef40604 100644 (file)
  */
 package jexer;
 
+import jexer.bits.CellAttributes;
+import jexer.bits.GraphicsChars;
+import jexer.event.TMouseEvent;
+
 /**
  * THScroller implements a simple horizontal scroll bar.
  */
 public final class THScroller extends TWidget {
 
+    /**
+     * Value that corresponds to being on the left edge of the scroll bar.
+     */
+    private int leftValue = 0;
+
+    /**
+     * Get the value that corresponds to being on the left edge of the scroll
+     * bar.
+     *
+     * @return the scroll value
+     */
+    public int getLeftValue() {
+        return leftValue;
+    }
+
+    /**
+     * Set the value that corresponds to being on the left edge of the
+     * scroll bar.
+     *
+     * @param leftValue the new scroll value
+     */
+    public void setLeftValue(final int leftValue) {
+        this.leftValue = leftValue;
+    }
+
+    /**
+     * Value that corresponds to being on the right edge of the scroll bar.
+     */
+    private int rightValue = 100;
+
+    /**
+     * Get the value that corresponds to being on the right edge of the
+     * scroll bar.
+     *
+     * @return the scroll value
+     */
+    public int getRightValue() {
+        return rightValue;
+    }
+
+    /**
+     * Set the value that corresponds to being on the right edge of the
+     * scroll bar.
+     *
+     * @param rightValue the new scroll value
+     */
+    public void setRightValue(final int rightValue) {
+        this.rightValue = rightValue;
+    }
+
+    /**
+     * Current value of the scroll.
+     */
+    private int value = 0;
+
+    /**
+     * Get current value of the scroll.
+     *
+     * @return the scroll value
+     */
+    public int getValue() {
+        return value;
+    }
+
+    /**
+     * Set current value of the scroll.
+     *
+     * @param value the new scroll value
+     */
+    public void setValue(final int value) {
+        this.value = value;
+    }
+
+    /**
+     * The increment for clicking on an arrow.
+     */
+    private int smallChange = 1;
+
+    /**
+     * Set the increment for clicking on an arrow.
+     *
+     * @param smallChange the new increment value
+     */
+    public void setSmallChange(final int smallChange) {
+        this.smallChange = smallChange;
+    }
+
+    /**
+     * The increment for clicking in the bar between the box and an arrow.
+     */
+    private int bigChange = 20;
+
+    /**
+     * Set the increment for clicking in the bar between the box and an
+     * arrow.
+     *
+     * @param bigChange the new increment value
+     */
+    public void setBigChange(final int bigChange) {
+        this.bigChange = bigChange;
+    }
+
+    /**
+     * When true, the user is dragging the scroll box.
+     */
+    private boolean inScroll = false;
+
+    /**
+     * Public constructor.
+     *
+     * @param parent parent widget
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param width height of scroll bar
+     */
+    public THScroller(final TWidget parent, final int x, final int y,
+        final int width) {
+
+        // Set parent and window
+        super(parent);
+
+        setX(x);
+        setY(y);
+        setHeight(1);
+        setWidth(width);
+    }
+
+    /**
+     * Compute the position of the scroll box (a.k.a. grip, thumb).
+     *
+     * @return Y position of the box, between 1 and width - 2
+     */
+    private int boxPosition() {
+        return (getWidth() - 3) * (value - leftValue) / (rightValue - leftValue) + 1;
+    }
+
+    /**
+     * Draw a horizontal scroll bar.
+     */
+    @Override
+    public void draw() {
+        CellAttributes arrowColor = getTheme().getColor("tscroller.arrows");
+        CellAttributes barColor = getTheme().getColor("tscroller.bar");
+        getScreen().putCharXY(0, 0, GraphicsChars.CP437[0x11], arrowColor);
+        getScreen().putCharXY(getWidth() - 1, 0, GraphicsChars.CP437[0x10],
+            arrowColor);
+
+        // Place the box
+        if (rightValue > leftValue) {
+            getScreen().hLineXY(1, 0, getWidth() - 2, GraphicsChars.CP437[0xB1],
+                barColor);
+            getScreen().putCharXY(boxPosition(), 0, GraphicsChars.BOX,
+                arrowColor);
+        } else {
+            getScreen().hLineXY(1, 0, getWidth() - 2, GraphicsChars.HATCH,
+                barColor);
+        }
+
+    }
+
+    /**
+     * Perform a small step change left.
+     */
+    public void decrement() {
+        if (leftValue == rightValue) {
+            return;
+        }
+        value -= smallChange;
+        if (value < leftValue) {
+            value = leftValue;
+        }
+    }
+
+    /**
+     * Perform a small step change right.
+     */
+    public void increment() {
+        if (leftValue == rightValue) {
+            return;
+        }
+        value += smallChange;
+        if (value > rightValue) {
+            value = rightValue;
+        }
+    }
+
+    /**
+     * Handle mouse button releases.
+     *
+     * @param mouse mouse button release event
+     */
+    @Override
+    public void onMouseUp(final TMouseEvent mouse) {
+
+        if (inScroll) {
+            inScroll = false;
+            return;
+        }
+
+        if (rightValue == leftValue) {
+            return;
+        }
+
+        if ((mouse.getX() == 0)
+            && (mouse.getY() == 0)
+        ) {
+            // Clicked on the left arrow
+            decrement();
+            return;
+        }
+
+        if ((mouse.getY() == 0)
+            && (mouse.getX() == getWidth() - 1)
+        ) {
+            // Clicked on the right arrow
+            increment();
+            return;
+        }
+
+        if ((mouse.getY() == 0)
+            && (mouse.getX() > 0)
+            && (mouse.getX() < boxPosition())
+        ) {
+            // Clicked between the left arrow and the box
+            value -= bigChange;
+            if (value < leftValue) {
+                value = leftValue;
+            }
+            return;
+        }
+
+        if ((mouse.getY() == 0)
+            && (mouse.getX() > boxPosition())
+            && (mouse.getX() < getWidth() - 1)
+        ) {
+            // Clicked between the box and the right arrow
+            value += bigChange;
+            if (value > rightValue) {
+                value = rightValue;
+            }
+            return;
+        }
+    }
+
+    /**
+     * Handle mouse movement events.
+     *
+     * @param mouse mouse motion event
+     */
+    @Override
+    public void onMouseMotion(final TMouseEvent mouse) {
+
+        if (rightValue == leftValue) {
+            inScroll = false;
+            return;
+        }
+
+        if ((mouse.getMouse1())
+            && (inScroll)
+            && (mouse.getX() > 0)
+            && (mouse.getX() < getWidth() - 1)
+        ) {
+            // Recompute value based on new box position
+            value = (rightValue - leftValue) * (mouse.getX()) / (getWidth() - 3) + leftValue;
+            return;
+        }
+        inScroll = false;
+    }
+
+    /**
+     * Handle mouse button press events.
+     *
+     * @param mouse mouse button press event
+     */
+    @Override
+    public void onMouseDown(final TMouseEvent mouse) {
+        if (rightValue == leftValue) {
+            inScroll = false;
+            return;
+        }
+
+        if ((mouse.getY() == 0)
+            && (mouse.getX() == boxPosition())
+        ) {
+            inScroll = true;
+            return;
+        }
+
+    }
+
 }
diff --git a/src/jexer/TText.java b/src/jexer/TText.java
new file mode 100644 (file)
index 0000000..1f1359f
--- /dev/null
@@ -0,0 +1,343 @@
+/**
+ * Jexer - Java Text User Interface
+ *
+ * License: LGPLv3 or later
+ *
+ * This module is licensed under the GNU Lesser General Public License
+ * Version 3.  Please see the file "COPYING" in this directory for more
+ * information about the GNU Lesser General Public License Version 3.
+ *
+ *     Copyright (C) 2015  Kevin Lamonte
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program; if not, see
+ * http://www.gnu.org/licenses/, or write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import jexer.bits.CellAttributes;
+import jexer.event.TKeypressEvent;
+import jexer.event.TMouseEvent;
+import static jexer.TKeypress.*;
+
+/**
+ * TText implements a simple text windget.
+ */
+public final class TText extends TWidget {
+
+    /**
+     * Text to display.
+     */
+    private String text;
+
+    /**
+     * Text converted to lines.
+     */
+    private List<String> lines;
+
+    /**
+     * Text color.
+     */
+    private String colorKey;
+
+    /**
+     * Vertical scrollbar.
+     */
+    private TVScroller vScroller;
+
+    /**
+     * Horizontal scrollbar.
+     */
+    private THScroller hScroller;
+
+    /**
+     * Maximum width of a single line.
+     */
+    private int maxLineWidth;
+
+    /**
+     * Number of lines between each paragraph.
+     */
+    private int lineSpacing = 1;
+
+    /**
+     * Convenience method used by TWindowLoggerOutput.
+     *
+     * @param line new line to add
+     */
+    public void addLine(final String line) {
+        if (text.length() == 0) {
+            text = line;
+        } else {
+            text += "\n\n";
+            text += line;
+        }
+        reflow();
+    }
+
+    /**
+     * Recompute the bounds for the scrollbars.
+     */
+    private void computeBounds() {
+        maxLineWidth = 0;
+        for (String line: lines) {
+            if (line.length() > maxLineWidth) {
+                maxLineWidth = line.length();
+            }
+        }
+
+        vScroller.setBottomValue(lines.size() - getHeight() + 1);
+        if (vScroller.getBottomValue() < 0) {
+            vScroller.setBottomValue(0);
+        }
+        if (vScroller.getValue() > vScroller.getBottomValue()) {
+            vScroller.setValue(vScroller.getBottomValue());
+        }
+
+        hScroller.setRightValue(maxLineWidth - getWidth() + 1);
+        if (hScroller.getRightValue() < 0) {
+            hScroller.setRightValue(0);
+        }
+        if (hScroller.getValue() > hScroller.getRightValue()) {
+            hScroller.setValue(hScroller.getRightValue());
+        }
+    }
+
+    /**
+     * Insert newlines into a string to wrap it to a maximum column.
+     * Terminate the final string with a newline.  Note that interior
+     * newlines are converted to spaces.
+     *
+     * @param str the string
+     * @param n the maximum number of characters in a line
+     * @return the wrapped string
+     */
+    private String wrap(final String str, final int n) {
+        assert (n > 0);
+
+        StringBuilder sb = new StringBuilder();
+        StringBuilder word = new StringBuilder();
+        int col = 0;
+        for (int i = 0; i < str.length(); i++) {
+            char ch = str.charAt(i);
+            if (ch == '\n') {
+                ch = ' ';
+            }
+            if (ch == ' ') {
+                sb.append(word.toString());
+                sb.append(ch);
+                if (word.length() >= n - 1) {
+                    sb.append('\n');
+                    col = 0;
+                }
+                word = new StringBuilder();
+            } else {
+                word.append(ch);
+            }
+
+            col++;
+            if (col >= n - 1) {
+                sb.append('\n');
+                col = 0;
+            }
+        }
+        sb.append(word.toString());
+        sb.append('\n');
+        return sb.toString();
+    }
+
+
+    /**
+     * Resize text and scrollbars for a new width/height.
+     */
+    public void reflow() {
+        // Reset the lines
+        lines.clear();
+
+        // Break up text into paragraphs
+        String [] paragraphs = text.split("\n\n");
+        for (String p: paragraphs) {
+            String paragraph = wrap(p, getWidth() - 1);
+            for (String line: paragraph.split("\n")) {
+                lines.add(line);
+            }
+            for (int i = 0; i < lineSpacing; i++) {
+                lines.add("");
+            }
+        }
+
+        // Start at the top
+        if (vScroller == null) {
+            vScroller = new TVScroller(this, getWidth() - 1, 0,
+                getHeight() - 1);
+            vScroller.setTopValue(0);
+            vScroller.setValue(0);
+        } else {
+            vScroller.setX(getWidth() - 1);
+            vScroller.setHeight(getHeight() - 1);
+        }
+        vScroller.setBigChange(getHeight() - 1);
+
+        // Start at the left
+        if (hScroller == null) {
+            hScroller = new THScroller(this, 0, getHeight() - 1,
+                getWidth() - 1);
+            hScroller.setLeftValue(0);
+            hScroller.setValue(0);
+        } else {
+            hScroller.setY(getHeight() - 1);
+            hScroller.setWidth(getWidth() - 1);
+        }
+        hScroller.setBigChange(getWidth() - 1);
+
+        computeBounds();
+    }
+
+    /**
+     * Public constructor.
+     *
+     * @param parent parent widget
+     * @param text text on the screen
+     * @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 TText(final TWidget parent, final String text, final int x,
+        final int y, final int width, final int height) {
+
+        this(parent, text, x, y, width, height, "ttext");
+    }
+
+    /**
+     * Public constructor.
+     *
+     * @param parent parent widget
+     * @param text text on the screen
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param width width of text area
+     * @param height height of text area
+     * @param colorKey ColorTheme key color to use for foreground text.
+     * Default is "ttext"
+     */
+    public TText(final TWidget parent, final String text, final int x,
+        final int y, final int width, final int height, final String colorKey) {
+
+        // Set parent and window
+        super(parent);
+
+        setX(x);
+        setY(y);
+        setWidth(width);
+        setHeight(height);
+        this.text = text;
+        this.colorKey = colorKey;
+
+        lines = new LinkedList<String>();
+
+        reflow();
+    }
+
+    /**
+     * Draw the text box.
+     */
+    @Override
+    public void draw() {
+        // Setup my color
+        CellAttributes color = getTheme().getColor(colorKey);
+
+        int begin = vScroller.getValue();
+        int topY = 0;
+        for (int i = begin; i < lines.size(); i++) {
+            String line = lines.get(i);
+            if (hScroller.getValue() < line.length()) {
+                line = line.substring(hScroller.getValue());
+            } else {
+                line = "";
+            }
+            String formatString = "%-" + Integer.toString(getWidth() - 1) + "s";
+            getScreen().putStrXY(0, topY, String.format(formatString, line),
+                color);
+            topY++;
+
+            if (topY >= getHeight() - 1) {
+                break;
+            }
+        }
+
+        // Pad the rest with blank lines
+        for (int i = topY; i < getHeight() - 1; i++) {
+            getScreen().hLineXY(0, i, getWidth() - 1, ' ', color);
+        }
+
+    }
+
+    /**
+     * Handle mouse press events.
+     *
+     * @param mouse mouse button press event
+     */
+    @Override
+    public void onMouseDown(final TMouseEvent mouse) {
+        if (mouse.getMouseWheelUp()) {
+            vScroller.decrement();
+            return;
+        }
+        if (mouse.getMouseWheelDown()) {
+            vScroller.increment();
+            return;
+        }
+
+        // Pass to children
+        super.onMouseDown(mouse);
+    }
+
+    /**
+     * Handle keystrokes.
+     *
+     * @param keypress keystroke event
+     */
+    @Override
+    public void onKeypress(final TKeypressEvent keypress) {
+        if (keypress.equals(kbLeft)) {
+            hScroller.decrement();
+        } else if (keypress.equals(kbRight)) {
+            hScroller.increment();
+        } else if (keypress.equals(kbUp)) {
+            vScroller.decrement();
+        } else if (keypress.equals(kbDown)) {
+            vScroller.increment();
+        } else if (keypress.equals(kbPgUp)) {
+            vScroller.bigDecrement();
+        } else if (keypress.equals(kbPgDn)) {
+            vScroller.bigIncrement();
+        } else if (keypress.equals(kbHome)) {
+            vScroller.toTop();
+        } else if (keypress.equals(kbEnd)) {
+            vScroller.toBottom();
+        } else {
+            // Pass other keys (tab etc.) on
+            super.onKeypress(keypress);
+        }
+    }
+
+}
index a0a591540fe4bcb68d66cd0857a0b84fdb36380f..459ef4fbe25986af4984cca3284eef368ad47ea9 100644 (file)
  */
 package jexer;
 
+import jexer.bits.CellAttributes;
+import jexer.bits.GraphicsChars;
+import jexer.event.TMouseEvent;
+
 /**
  * TVScroller implements a simple vertical scroll bar.
  */
 public final class TVScroller extends TWidget {
 
+    /**
+     * Value that corresponds to being on the top edge of the scroll bar.
+     */
+    private int topValue = 0;
+
+    /**
+     * Get the value that corresponds to being on the top edge of the scroll
+     * bar.
+     *
+     * @return the scroll value
+     */
+    public int getTopValue() {
+        return topValue;
+    }
+
+    /**
+     * Set the value that corresponds to being on the top edge of the scroll
+     * bar.
+     *
+     * @param topValue the new scroll value
+     */
+    public void setTopValue(final int topValue) {
+        this.topValue = topValue;
+    }
+
+    /**
+     * Value that corresponds to being on the bottom edge of the scroll bar.
+     */
+    private int bottomValue = 100;
+
+    /**
+     * Get the value that corresponds to being on the bottom edge of the
+     * scroll bar.
+     *
+     * @return the scroll value
+     */
+    public int getBottomValue() {
+        return bottomValue;
+    }
+
+    /**
+     * Set the value that corresponds to being on the bottom edge of the
+     * scroll bar.
+     *
+     * @param bottomValue the new scroll value
+     */
+    public void setBottomValue(final int bottomValue) {
+        this.bottomValue = bottomValue;
+    }
+
+    /**
+     * Current value of the scroll.
+     */
+    private int value = 0;
+
+    /**
+     * Get current value of the scroll.
+     *
+     * @return the scroll value
+     */
+    public int getValue() {
+        return value;
+    }
+
+    /**
+     * Set current value of the scroll.
+     *
+     * @param value the new scroll value
+     */
+    public void setValue(final int value) {
+        this.value = value;
+    }
+
+    /**
+     * The increment for clicking on an arrow.
+     */
+    private int smallChange = 1;
+
+    /**
+     * Set the increment for clicking on an arrow.
+     *
+     * @param smallChange the new increment value
+     */
+    public void setSmallChange(final int smallChange) {
+        this.smallChange = smallChange;
+    }
+
+    /**
+     * The increment for clicking in the bar between the box and an arrow.
+     */
+    private int bigChange = 20;
+
+    /**
+     * Set the increment for clicking in the bar between the box and an
+     * arrow.
+     *
+     * @param bigChange the new increment value
+     */
+    public void setBigChange(final int bigChange) {
+        this.bigChange = bigChange;
+    }
+
+    /**
+     * When true, the user is dragging the scroll box.
+     */
+    private boolean inScroll = false;
+
+    /**
+     * Public constructor.
+     *
+     * @param parent parent widget
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param width height of scroll bar
+     */
+    public TVScroller(final TWidget parent, final int x, final int y,
+        final int height) {
+
+        // Set parent and window
+        super(parent);
+
+        setX(x);
+        setY(y);
+        setHeight(height);
+        setWidth(1);
+    }
+
+    /**
+     * Compute the position of the scroll box (a.k.a. grip, thumb).
+     *
+     * @param Y position of the box, between 1 and height - 2
+     */
+    private int boxPosition() {
+        return (getHeight() - 3) * (value - topValue) / (bottomValue - topValue) + 1;
+    }
+
+    /**
+     * Draw a vertical scroll bar.
+     */
+    @Override
+    public void draw() {
+        CellAttributes arrowColor = getTheme().getColor("tscroller.arrows");
+        CellAttributes barColor = getTheme().getColor("tscroller.bar");
+        getScreen().putCharXY(0, 0, GraphicsChars.CP437[0x1E], arrowColor);
+        getScreen().putCharXY(0, getHeight() - 1, GraphicsChars.CP437[0x1F],
+            arrowColor);
+
+        // Place the box
+        if (bottomValue > topValue) {
+            getScreen().vLineXY(0, 1, getHeight() - 2,
+                GraphicsChars.CP437[0xB1], barColor);
+            getScreen().putCharXY(0, boxPosition(), GraphicsChars.BOX,
+                arrowColor);
+        } else {
+            getScreen().vLineXY(0, 1, getHeight() - 2, GraphicsChars.HATCH,
+                barColor);
+        }
+
+    }
+
+    /**
+     * Perform a small step change up.
+     */
+    public void decrement() {
+        if (bottomValue == topValue) {
+            return;
+        }
+        value -= smallChange;
+        if (value < topValue) {
+            value = topValue;
+        }
+    }
+
+    /**
+     * Perform a small step change down.
+     */
+    public void increment() {
+        if (bottomValue == topValue) {
+            return;
+        }
+        value += smallChange;
+        if (value > bottomValue) {
+            value = bottomValue;
+        }
+    }
+
+    /**
+     * Perform a big step change up.
+     */
+    public void bigDecrement() {
+        if (bottomValue == topValue) {
+            return;
+        }
+        value -= bigChange;
+        if (value < topValue) {
+            value = topValue;
+        }
+    }
+
+    /**
+     * Perform a big step change down.
+     */
+    public void bigIncrement() {
+        if (bottomValue == topValue) {
+            return;
+        }
+        value += bigChange;
+        if (value > bottomValue) {
+            value = bottomValue;
+        }
+    }
+
+    /**
+     * Go to the top edge of the scroller.
+     */
+    public void toTop() {
+        value = topValue;
+    }
+
+    /**
+     * Go to the bottom edge of the scroller.
+     */
+    public void toBottom() {
+        value = bottomValue;
+    }
+
+    /**
+     * Handle mouse button releases.
+     *
+     * @param mouse mouse button release event
+     */
+    @Override
+    public void onMouseUp(final TMouseEvent mouse) {
+        if (bottomValue == topValue) {
+            return;
+        }
+
+        if (inScroll) {
+            inScroll = false;
+            return;
+        }
+
+        if ((mouse.getX() == 0)
+            && (mouse.getY() == 0)
+        ) {
+            // Clicked on the top arrow
+            decrement();
+            return;
+        }
+
+        if ((mouse.getX() == 0)
+            && (mouse.getY() == getHeight() - 1)
+        ) {
+            // Clicked on the bottom arrow
+            increment();
+            return;
+        }
+
+        if ((mouse.getX() == 0)
+            && (mouse.getY() > 0)
+            && (mouse.getY() < boxPosition())
+        ) {
+            // Clicked between the top arrow and the box
+            value -= bigChange;
+            if (value < topValue) {
+                value = topValue;
+            }
+            return;
+        }
+
+        if ((mouse.getX() == 0)
+            && (mouse.getY() > boxPosition())
+            && (mouse.getY() < getHeight() - 1)
+        ) {
+            // Clicked between the box and the bottom arrow
+            value += bigChange;
+            if (value > bottomValue) {
+                value = bottomValue;
+            }
+            return;
+        }
+    }
+
+    /**
+     * Handle mouse movement events.
+     *
+     * @param mouse mouse motion event
+     */
+    @Override
+    public void onMouseMotion(final TMouseEvent mouse) {
+        if (bottomValue == topValue) {
+            return;
+        }
+
+        if ((mouse.getMouse1()) &&
+            (inScroll) &&
+            (mouse.getY() > 0) &&
+            (mouse.getY() < getHeight() - 1)
+        ) {
+            // Recompute value based on new box position
+            value = (bottomValue - topValue) * (mouse.getY()) / (getHeight() - 3) + topValue;
+            return;
+        }
+
+        inScroll = false;
+    }
+
+    /**
+     * Handle mouse press events.
+     *
+     * @param mouse mouse button press event
+     */
+    @Override
+    public void onMouseDown(final TMouseEvent mouse) {
+        if (bottomValue == topValue) {
+            return;
+        }
+
+        if ((mouse.getX() == 0)
+            && (mouse.getY() == boxPosition())
+        ) {
+            inScroll = true;
+            return;
+        }
+    }
+
 }
index 25e43f71f7e651d503709214cd68d656467c0d8f..84277a8c2acc8dba6d46ff018ddbfbdc232404ce 100644 (file)
@@ -1097,4 +1097,40 @@ public abstract class TWidget implements Comparable<TWidget> {
             updateAction);
     }
 
+    /**
+     * Convenience function to add a scrollable text box to this
+     * container/window.
+     *
+     * @param text text on the screen
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param width width of text area
+     * @param height height of text area
+     * @param colorKey ColorTheme key color to use for foreground text
+     * @return the new text box
+     */
+    public TText addText(final String text, final int x,
+        final int y, final int width, final int height, final String colorKey) {
+
+        return new TText(this, text, x, y, width, height, colorKey);
+    }
+
+    /**
+     * Convenience function to add a scrollable text box to this
+     * container/window.
+     *
+     * @param text text on the screen
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param width width of text area
+     * @param height height of text area
+     * @return the new text box
+     */
+    public TText addText(final String text, final int x, final int y,
+        final int width, final int height) {
+
+        return new TText(this, text, x, y, width, height, "ttext");
+    }
+
+
 }