Common Scrollable interface
authorKevin Lamonte <kevin.lamonte@gmail.com>
Sun, 16 Jul 2017 17:24:20 +0000 (13:24 -0400)
committerKevin Lamonte <kevin.lamonte@gmail.com>
Sun, 16 Jul 2017 17:24:20 +0000 (13:24 -0400)
16 files changed:
docs/TODO.md
src/jexer/Scrollable.java [new file with mode: 0644]
src/jexer/TApplication.java
src/jexer/TDirectoryTreeItem.java
src/jexer/TFileOpenBox.java
src/jexer/THScroller.java
src/jexer/TList.java
src/jexer/TScrollableWidget.java [new file with mode: 0644]
src/jexer/TScrollableWindow.java [new file with mode: 0644]
src/jexer/TTerminalWindow.java
src/jexer/TText.java
src/jexer/TTreeItem.java
src/jexer/TTreeView.java
src/jexer/TVScroller.java
src/jexer/demos/DemoTextWindow.java
src/jexer/demos/DemoTreeViewWindow.java

index 29e5194f61d6a7f3606dd40373876a4522d7b2dd..a3455e5ea1fe1c2c17ecd1f7ce5d02b465aca2b2 100644 (file)
@@ -11,22 +11,9 @@ BUG: TTreeView.reflow() doesn't keep the vertical dot within the
 0.0.5
 
 
-- Scrollable:
-  - TTreeView
-  - TText
-  - TList
-  - TTerminalWindow
-  - TScrollableWindow
-
-
 - TWindow:
   - UNCLOSABLE (#8)
-  - H/V scrollbars (#4)
-    - Expose toTop()/toLeft()/...
 
-- TText:
-  - Scrollbars adjust automatically
-    - Expose toTop()/toLeft()/...
 
 - TEditor
 - Eliminate all Eclipse warnings
diff --git a/src/jexer/Scrollable.java b/src/jexer/Scrollable.java
new file mode 100644 (file)
index 0000000..5a06582
--- /dev/null
@@ -0,0 +1,284 @@
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2017 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 jexer.event.TKeypressEvent;
+import jexer.event.TMouseEvent;
+import static jexer.TKeypress.*;
+
+/**
+ * Scrollable provides a public API for horizontal and vertical scrollbars.
+ * Note that not all Scrollables support both horizontal and vertical
+ * scrolling; for those that only support a subset, it is expected that the
+ * methods corresponding to the missing scrollbar quietly succeed without
+ * throwing any exceptions.
+ */
+public interface Scrollable {
+
+    /**
+     * Get the horizontal scrollbar, or null if this Viewport does not
+     * support horizontal scrolling.
+     *
+     * @return the horizontal scrollbar
+     */
+    public THScroller getHorizontalScroller();
+
+    /**
+     * Get the vertical scrollbar, or null if this Viewport does not support
+     * vertical scrolling.
+     *
+     * @return the vertical scrollbar
+     */
+    public TVScroller getVerticalScroller();
+
+    /**
+     * Get the value that corresponds to being on the top edge of the
+     * vertical scroll bar.
+     *
+     * @return the scroll value
+     */
+    public int getTopValue();
+
+    /**
+     * Set the value that corresponds to being on the top edge of the
+     * vertical scroll bar.
+     *
+     * @param topValue the new scroll value
+     */
+    public void setTopValue(final int topValue);
+
+    /**
+     * Get the value that corresponds to being on the bottom edge of the
+     * vertical scroll bar.
+     *
+     * @return the scroll value
+     */
+    public int getBottomValue();
+
+    /**
+     * Set the value that corresponds to being on the bottom edge of the
+     * vertical scroll bar.
+     *
+     * @param bottomValue the new scroll value
+     */
+    public void setBottomValue(final int bottomValue);
+
+    /**
+     * Get current value of the vertical scroll.
+     *
+     * @return the scroll value
+     */
+    public int getVerticalValue();
+
+    /**
+     * Set current value of the vertical scroll.
+     *
+     * @param value the new scroll value
+     */
+    public void setVerticalValue(final int value);
+
+    /**
+     * Get the increment for clicking on an arrow on the vertical scrollbar.
+     *
+     * @return the increment value
+     */
+    public int getVerticalSmallChange();
+
+    /**
+     * Set the increment for clicking on an arrow on the vertical scrollbar.
+     *
+     * @param smallChange the new increment value
+     */
+    public void setVerticalSmallChange(final int smallChange);
+
+    /**
+     * Get the increment for clicking in the bar between the box and an
+     * arrow on the vertical scrollbar.
+     *
+     * @return the increment value
+     */
+    public int getVerticalBigChange();
+
+    /**
+     * Set the increment for clicking in the bar between the box and an
+     * arrow on the vertical scrollbar.
+     *
+     * @param bigChange the new increment value
+     */
+    public void setVerticalBigChange(final int bigChange);
+
+    /**
+     * Perform a small step change up.
+     */
+    public void verticalDecrement();
+
+    /**
+     * Perform a small step change down.
+     */
+    public void verticalIncrement();
+
+    /**
+     * Perform a big step change up.
+     */
+    public void bigVerticalDecrement();
+
+    /**
+     * Perform a big step change down.
+     */
+    public void bigVerticalIncrement();
+
+    /**
+     * Go to the top edge of the vertical scroller.
+     */
+    public void toTop();
+
+    /**
+     * Go to the bottom edge of the vertical scroller.
+     */
+    public void toBottom();
+
+    /**
+     * Get the value that corresponds to being on the left edge of the
+     * horizontal scroll bar.
+     *
+     * @return the scroll value
+     */
+    public int getLeftValue();
+
+    /**
+     * Set the value that corresponds to being on the left edge of the
+     * horizontal scroll bar.
+     *
+     * @param leftValue the new scroll value
+     */
+    public void setLeftValue(final int leftValue);
+
+    /**
+     * Get the value that corresponds to being on the right edge of the
+     * horizontal scroll bar.
+     *
+     * @return the scroll value
+     */
+    public int getRightValue();
+
+    /**
+     * Set the value that corresponds to being on the right edge of the
+     * horizontal scroll bar.
+     *
+     * @param rightValue the new scroll value
+     */
+    public void setRightValue(final int rightValue);
+
+    /**
+     * Get current value of the horizontal scroll.
+     *
+     * @return the scroll value
+     */
+    public int getHorizontalValue();
+
+    /**
+     * Set current value of the horizontal scroll.
+     *
+     * @param value the new scroll value
+     */
+    public void setHorizontalValue(final int value);
+
+    /**
+     * Get the increment for clicking on an arrow on the horizontal
+     * scrollbar.
+     *
+     * @return the increment value
+     */
+    public int getHorizontalSmallChange();
+
+    /**
+     * Set the increment for clicking on an arrow on the horizontal
+     * scrollbar.
+     *
+     * @param smallChange the new increment value
+     */
+    public void setHorizontalSmallChange(final int smallChange);
+
+    /**
+     * Get the increment for clicking in the bar between the box and an
+     * arrow on the horizontal scrollbar.
+     *
+     * @return the increment value
+     */
+    public int getHorizontalBigChange();
+
+    /**
+     * Set the increment for clicking in the bar between the box and an
+     * arrow on the horizontal scrollbar.
+     *
+     * @param bigChange the new increment value
+     */
+    public void setHorizontalBigChange(final int bigChange);
+
+    /**
+     * Perform a small step change left.
+     */
+    public void horizontalDecrement();
+
+    /**
+     * Perform a small step change right.
+     */
+    public void horizontalIncrement();
+
+    /**
+     * Perform a big step change left.
+     */
+    public void bigHorizontalDecrement();
+
+    /**
+     * Perform a big step change right.
+     */
+    public void bigHorizontalIncrement();
+
+    /**
+     * Go to the left edge of the horizontal scroller.
+     */
+    public void toLeft();
+
+    /**
+     * Go to the right edge of the horizontal scroller.
+     */
+    public void toRight();
+
+    /**
+     * Go to the top-left edge of the horizontal and vertical scrollers.
+     */
+    public void toHome();
+
+    /**
+     * Go to the bottom-right edge of the horizontal and vertical scrollers.
+     */
+    public void toEnd();
+
+}
index 6d71dc7771327d84b6135c75054b22f36d2033f4..b80e7a30dd483bebfd6fc130da3b091810463b5e 100644 (file)
@@ -2609,7 +2609,6 @@ public class TApplication implements Runnable {
     /**
      * Convenience function to create a new window and make it active.
      *
-     * @param application TApplication that manages this window
      * @param title window title, will be centered along the top border
      * @param x column relative to parent
      * @param y row relative to parent
index 1cf377eb9f3f4f3583b303597bfb4a15d8a0c9fb..6d5d018004775c97ae1b476018c5c30f398ce996 100644 (file)
@@ -76,7 +76,7 @@ public class TDirectoryTreeItem extends TTreeItem {
         setExpandable(true);
 
         if (!isExpanded() || !isExpandable()) {
-            getTreeView().reflow();
+            getTreeView().reflowData();
             return;
         }
 
@@ -103,7 +103,7 @@ public class TDirectoryTreeItem extends TTreeItem {
         }
         Collections.sort(getChildren());
 
-        getTreeView().reflow();
+        getTreeView().reflowData();
     }
 
     /**
@@ -212,6 +212,6 @@ public class TDirectoryTreeItem extends TTreeItem {
             getTreeView().setSelected(childFile);
             setExpanded(oldExpanded);
         }
-        getTreeView().reflow();
+        getTreeView().reflowData();
     }
 }
index ebf2daacaa2351a137f275736aad5d877f408f3a..0a69a972674fbc7f0b62d09c0239d356d8fea090 100644 (file)
@@ -126,7 +126,7 @@ public final class TFileOpenBox extends TWindow {
                 treeViewRoot = new TDirectoryTreeItem(treeView, newFilename,
                     true);
                 treeView.setTreeRoot(treeViewRoot, true);
-                treeView.reflow();
+                treeView.reflowData();
                 openButton.setEnabled(false);
                 directoryList.setPath(newFilename);
             }
index d488ebf417db22b156484951fb856a31fb0a64bb..9e9b372ec51854d8bf75e968746d65e58ef1c8c8 100644 (file)
@@ -115,6 +115,15 @@ public final class THScroller extends TWidget {
      */
     private int smallChange = 1;
 
+    /**
+     * Get the increment for clicking on an arrow.
+     *
+     * @return the increment value
+     */
+    public int getSmallChange() {
+        return smallChange;
+    }
+
     /**
      * Set the increment for clicking on an arrow.
      *
@@ -129,6 +138,16 @@ public final class THScroller extends TWidget {
      */
     private int bigChange = 20;
 
+    /**
+     * Set the increment for clicking in the bar between the box and an
+     * arrow.
+     *
+     * @return the increment value
+     */
+    public int getBigChange() {
+        return bigChange;
+    }
+
     /**
      * Set the increment for clicking in the bar between the box and an
      * arrow.
@@ -218,6 +237,46 @@ public final class THScroller extends TWidget {
         }
     }
 
+    /**
+     * Perform a big step change left.
+     */
+    public void bigDecrement() {
+        if (leftValue == rightValue) {
+            return;
+        }
+        value -= bigChange;
+        if (value < leftValue) {
+            value = leftValue;
+        }
+    }
+
+    /**
+     * Perform a big step change right.
+     */
+    public void bigIncrement() {
+        if (rightValue == leftValue) {
+            return;
+        }
+        value += bigChange;
+        if (value > rightValue) {
+            value = rightValue;
+        }
+    }
+
+    /**
+     * Go to the left edge of the scroller.
+     */
+    public void toLeft() {
+        value = leftValue;
+    }
+
+    /**
+     * Go to the right edge of the scroller.
+     */
+    public void toRight() {
+        value = rightValue;
+    }
+
     /**
      * Handle mouse button releases.
      *
index 2312889722707ee6d6ea06f3dfcc5523c59c0314..71600485fe2b9cb1646a8df8a3c0fa5c7af5755d 100644 (file)
@@ -39,7 +39,7 @@ import static jexer.TKeypress.*;
 /**
  * TList shows a list of strings, and lets the user select one.
  */
-public class TList extends TWidget {
+public class TList extends TScrollableWidget {
 
     /**
      * The list of strings to display.
@@ -89,35 +89,7 @@ public class TList extends TWidget {
     public final void setList(final List<String> list) {
         strings.clear();
         strings.addAll(list);
-        reflow();
-    }
-
-    /**
-     * Vertical scrollbar.
-     */
-    private TVScroller vScroller;
-
-    /**
-     * Get the vertical scrollbar.  This is used by subclasses.
-     *
-     * @return the vertical scrollbar
-     */
-    public final TVScroller getVScroller() {
-        return vScroller;
-    }
-
-    /**
-     * Horizontal scrollbar.
-     */
-    private THScroller hScroller;
-
-    /**
-     * Get the horizontal scrollbar.  This is used by subclasses.
-     *
-     * @return the horizontal scrollbar
-     */
-    public final THScroller getHScroller() {
-        return hScroller;
+        reflowData();
     }
 
     /**
@@ -160,7 +132,8 @@ public class TList extends TWidget {
     /**
      * Resize for a new width/height.
      */
-    public void reflow() {
+    @Override
+    public void reflowData() {
 
         // Reset the lines
         selectedString = -1;
@@ -173,37 +146,15 @@ public class TList extends TWidget {
             }
         }
 
-        // Start at the top
-        if (vScroller == null) {
-            vScroller = new TVScroller(this, getWidth() - 1, 0,
-                getHeight() - 1);
-        } else {
-            vScroller.setX(getWidth() - 1);
-            vScroller.setHeight(getHeight() - 1);
+        setBottomValue(strings.size() - getHeight() + 1);
+        if (getBottomValue() < 0) {
+            setBottomValue(0);
         }
-        vScroller.setBottomValue(strings.size() - getHeight() + 1);
-        vScroller.setTopValue(0);
-        vScroller.setValue(0);
-        if (vScroller.getBottomValue() < 0) {
-            vScroller.setBottomValue(0);
-        }
-        vScroller.setBigChange(getHeight() - 1);
 
-        // Start at the left
-        if (hScroller == null) {
-            hScroller = new THScroller(this, 0, getHeight() - 1,
-                getWidth() - 1);
-        } else {
-            hScroller.setY(getHeight() - 1);
-            hScroller.setWidth(getWidth() - 1);
-        }
-        hScroller.setRightValue(maxLineWidth - getWidth() + 1);
-        hScroller.setLeftValue(0);
-        hScroller.setValue(0);
-        if (hScroller.getRightValue() < 0) {
-            hScroller.setRightValue(0);
+        setRightValue(maxLineWidth - getWidth() + 1);
+        if (getRightValue() < 0) {
+            setRightValue(0);
         }
-        hScroller.setBigChange(getWidth() - 1);
     }
 
     /**
@@ -244,7 +195,10 @@ public class TList extends TWidget {
         if (strings != null) {
             this.strings.addAll(strings);
         }
-        reflow();
+
+        hScroller = new THScroller(this, 0, getHeight() - 1, getWidth() - 1);
+        vScroller = new TVScroller(this, getWidth() - 1, 0, getHeight() - 1);
+        reflowData();
     }
 
     /**
@@ -272,7 +226,10 @@ public class TList extends TWidget {
         if (strings != null) {
             this.strings.addAll(strings);
         }
-        reflow();
+
+        hScroller = new THScroller(this, 0, getHeight() - 1, getWidth() - 1);
+        vScroller = new TVScroller(this, getWidth() - 1, 0, getHeight() - 1);
+        reflowData();
     }
 
     /**
@@ -281,12 +238,12 @@ public class TList extends TWidget {
     @Override
     public void draw() {
         CellAttributes color = null;
-        int begin = vScroller.getValue();
+        int begin = getVerticalValue();
         int topY = 0;
         for (int i = begin; i < strings.size(); i++) {
             String line = strings.get(i);
-            if (hScroller.getValue() < line.length()) {
-                line = line.substring(hScroller.getValue());
+            if (getHorizontalValue() < line.length()) {
+                line = line.substring(getHorizontalValue());
             } else {
                 line = "";
             }
@@ -326,20 +283,20 @@ public class TList extends TWidget {
     @Override
     public void onMouseDown(final TMouseEvent mouse) {
         if (mouse.isMouseWheelUp()) {
-            vScroller.decrement();
+            verticalDecrement();
             return;
         }
         if (mouse.isMouseWheelDown()) {
-            vScroller.increment();
+            verticalIncrement();
             return;
         }
 
         if ((mouse.getX() < getWidth() - 1)
             && (mouse.getY() < getHeight() - 1)) {
-            if (vScroller.getValue() + mouse.getY() < strings.size()) {
-                selectedString = vScroller.getValue() + mouse.getY();
+            if (getVerticalValue() + mouse.getY() < strings.size()) {
+                selectedString = getVerticalValue() + mouse.getY();
+                dispatchEnter();
             }
-            dispatchEnter();
             return;
         }
 
@@ -355,15 +312,15 @@ public class TList extends TWidget {
     @Override
     public void onKeypress(final TKeypressEvent keypress) {
         if (keypress.equals(kbLeft)) {
-            hScroller.decrement();
+            horizontalDecrement();
         } else if (keypress.equals(kbRight)) {
-            hScroller.increment();
+            horizontalIncrement();
         } else if (keypress.equals(kbUp)) {
             if (strings.size() > 0) {
                 if (selectedString >= 0) {
                     if (selectedString > 0) {
-                        if (selectedString - vScroller.getValue() == 0) {
-                            vScroller.decrement();
+                        if (selectedString - getVerticalValue() == 0) {
+                            verticalDecrement();
                         }
                         selectedString--;
                     }
@@ -379,8 +336,8 @@ public class TList extends TWidget {
                 if (selectedString >= 0) {
                     if (selectedString < strings.size() - 1) {
                         selectedString++;
-                        if (selectedString - vScroller.getValue() == getHeight() - 1) {
-                            vScroller.increment();
+                        if (selectedString - getVerticalValue() == getHeight() - 1) {
+                            verticalIncrement();
                         }
                     }
                 } else {
@@ -391,7 +348,7 @@ public class TList extends TWidget {
                 dispatchMove();
             }
         } else if (keypress.equals(kbPgUp)) {
-            vScroller.bigDecrement();
+            bigVerticalDecrement();
             if (selectedString >= 0) {
                 selectedString -= getHeight() - 1;
                 if (selectedString < 0) {
@@ -402,7 +359,7 @@ public class TList extends TWidget {
                 dispatchMove();
             }
         } else if (keypress.equals(kbPgDn)) {
-            vScroller.bigIncrement();
+            bigVerticalIncrement();
             if (selectedString >= 0) {
                 selectedString += getHeight() - 1;
                 if (selectedString > strings.size() - 1) {
@@ -413,7 +370,7 @@ public class TList extends TWidget {
                 dispatchMove();
             }
         } else if (keypress.equals(kbHome)) {
-            vScroller.toTop();
+            toTop();
             if (strings.size() > 0) {
                 selectedString = 0;
             }
@@ -421,7 +378,7 @@ public class TList extends TWidget {
                 dispatchMove();
             }
         } else if (keypress.equals(kbEnd)) {
-            vScroller.toBottom();
+            toBottom();
             if (strings.size() > 0) {
                 selectedString = strings.size() - 1;
             }
diff --git a/src/jexer/TScrollableWidget.java b/src/jexer/TScrollableWidget.java
new file mode 100644 (file)
index 0000000..a4ba70a
--- /dev/null
@@ -0,0 +1,593 @@
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2017 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 jexer.event.TResizeEvent;
+
+/**
+ * TScrollableWidget is a convenience superclass for widgets that have
+ * scrollbars.
+ */
+public class TScrollableWidget extends TWidget implements Scrollable {
+
+    /**
+     * The horizontal scrollbar.
+     */
+    protected THScroller hScroller = null;
+
+    /**
+     * The vertical scrollbar.
+     */
+    protected TVScroller vScroller = null;
+
+    /**
+     * 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() - 1);
+            vScroller.setBigChange(getHeight() - 1);
+        }
+    }
+
+    /**
+     * Recompute whatever data is displayed by this widget.
+     */
+    public void reflowData() {
+        // Default: nothing to do
+    }
+
+    /**
+     * Handle window/screen resize events.
+     *
+     * @param event resize event
+     */
+    @Override
+    public void onResize(final TResizeEvent event) {
+        if (event.getType() == TResizeEvent.Type.WIDGET) {
+            setWidth(event.getWidth());
+            setHeight(event.getHeight());
+
+            reflowData();
+            placeScrollbars();
+            return;
+        } else {
+            super.onResize(event);
+        }
+    }
+
+    /**
+     * Protected constructor.
+     *
+     * @param parent parent widget
+     */
+    protected TScrollableWidget(final TWidget parent) {
+        super(parent);
+    }
+
+    /**
+     * Protected constructor.
+     *
+     * @param parent parent widget
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param width width of widget
+     * @param height height of widget
+     */
+    protected TScrollableWidget(final TWidget parent, final int x, final int y,
+        final int width, final int height) {
+
+        super(parent, x, y, width, height);
+    }
+
+    /**
+     * Protected constructor used by subclasses that are disabled by default.
+     *
+     * @param parent parent widget
+     * @param enabled if true assume enabled
+     */
+    protected TScrollableWidget(final TWidget parent, final boolean enabled) {
+
+        super(parent, enabled);
+    }
+
+    /**
+     * Protected constructor used by subclasses that are disabled by default.
+     *
+     * @param parent parent widget
+     * @param enabled if true assume enabled
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param width width of widget
+     * @param height height of widget
+     */
+    protected TScrollableWidget(final TWidget parent, final boolean enabled,
+        final int x, final int y, final int width, final int height) {
+
+        super(parent, enabled, x, y, width, height);
+    }
+
+    /**
+     * Get the horizontal scrollbar, or null if this Viewport does not
+     * support horizontal scrolling.
+     *
+     * @return the horizontal scrollbar
+     */
+    public THScroller getHorizontalScroller() {
+        return hScroller;
+    }
+
+    /**
+     * Get the vertical scrollbar, or null if this Viewport does not support
+     * vertical scrolling.
+     *
+     * @return the vertical scrollbar
+     */
+    public TVScroller getVerticalScroller() {
+        return vScroller;
+    }
+
+    /**
+     * Get the value that corresponds to being on the top edge of the
+     * vertical scroll bar.
+     *
+     * @return the scroll value
+     */
+    public int getTopValue() {
+        if (vScroller == null) {
+            return 0;
+        } else {
+            return vScroller.getTopValue();
+        }
+    }
+
+    /**
+     * Set the value that corresponds to being on the top edge of the
+     * vertical scroll bar.
+     *
+     * @param topValue the new scroll value
+     */
+    public void setTopValue(final int topValue) {
+        if (vScroller == null) {
+            return;
+        } else {
+            vScroller.setTopValue(topValue);
+        }
+    }
+
+    /**
+     * Get the value that corresponds to being on the bottom edge of the
+     * vertical scroll bar.
+     *
+     * @return the scroll value
+     */
+    public int getBottomValue() {
+        if (vScroller == null) {
+            return 0;
+        } else {
+            return vScroller.getBottomValue();
+        }
+    }
+
+    /**
+     * Set the value that corresponds to being on the bottom edge of the
+     * vertical scroll bar.
+     *
+     * @param bottomValue the new scroll value
+     */
+    public void setBottomValue(final int bottomValue) {
+        if (vScroller == null) {
+            return;
+        } else {
+            vScroller.setBottomValue(bottomValue);
+        }
+    }
+
+    /**
+     * Get current value of the vertical scroll.
+     *
+     * @return the scroll value
+     */
+    public int getVerticalValue() {
+        if (vScroller == null) {
+            return 0;
+        } else {
+            return vScroller.getValue();
+        }
+    }
+
+    /**
+     * Set current value of the vertical scroll.
+     *
+     * @param value the new scroll value
+     */
+    public void setVerticalValue(final int value) {
+        if (vScroller == null) {
+            return;
+        } else {
+            vScroller.setValue(value);
+        }
+    }
+
+    /**
+     * Get the increment for clicking on an arrow on the vertical scrollbar.
+     *
+     * @return the increment value
+     */
+    public int getVerticalSmallChange() {
+        if (vScroller == null) {
+            return 0;
+        } else {
+            return vScroller.getSmallChange();
+        }
+    }
+
+    /**
+     * Set the increment for clicking on an arrow on the vertical scrollbar.
+     *
+     * @param smallChange the new increment value
+     */
+    public void setVerticalSmallChange(final int smallChange) {
+        if (vScroller == null) {
+            return;
+        } else {
+            vScroller.setSmallChange(smallChange);
+        }
+    }
+
+    /**
+     * Get the increment for clicking in the bar between the box and an
+     * arrow on the vertical scrollbar.
+     *
+     * @return the increment value
+     */
+    public int getVerticalBigChange() {
+        if (vScroller == null) {
+            return 0;
+        } else {
+            return vScroller.getBigChange();
+        }
+    }
+
+    /**
+     * Set the increment for clicking in the bar between the box and an
+     * arrow on the vertical scrollbar.
+     *
+     * @param bigChange the new increment value
+     */
+    public void setVerticalBigChange(final int bigChange) {
+        if (vScroller == null) {
+            return;
+        } else {
+            vScroller.setBigChange(bigChange);
+        }
+    }
+
+    /**
+     * Perform a small step change up.
+     */
+    public void verticalDecrement() {
+        if (vScroller == null) {
+            return;
+        } else {
+            vScroller.decrement();
+        }
+    }
+
+    /**
+     * Perform a small step change down.
+     */
+    public void verticalIncrement() {
+        if (vScroller == null) {
+            return;
+        } else {
+            vScroller.increment();
+        }
+    }
+
+    /**
+     * Perform a big step change up.
+     */
+    public void bigVerticalDecrement() {
+        if (vScroller == null) {
+            return;
+        } else {
+            vScroller.bigDecrement();
+        }
+    }
+
+    /**
+     * Perform a big step change down.
+     */
+    public void bigVerticalIncrement() {
+        if (vScroller == null) {
+            return;
+        } else {
+            vScroller.bigIncrement();
+        }
+    }
+
+    /**
+     * Go to the top edge of the vertical scroller.
+     */
+    public void toTop() {
+        if (vScroller == null) {
+            return;
+        } else {
+            vScroller.toTop();
+        }
+    }
+
+    /**
+     * Go to the bottom edge of the vertical scroller.
+     */
+    public void toBottom() {
+        if (vScroller == null) {
+            return;
+        } else {
+            vScroller.toBottom();
+        }
+    }
+
+    /**
+     * Get the value that corresponds to being on the left edge of the
+     * horizontal scroll bar.
+     *
+     * @return the scroll value
+     */
+    public int getLeftValue() {
+        if (hScroller == null) {
+            return 0;
+        } else {
+            return hScroller.getLeftValue();
+        }
+    }
+
+    /**
+     * Set the value that corresponds to being on the left edge of the
+     * horizontal scroll bar.
+     *
+     * @param leftValue the new scroll value
+     */
+    public void setLeftValue(final int leftValue) {
+        if (hScroller == null) {
+            return;
+        } else {
+            hScroller.setLeftValue(leftValue);
+        }
+    }
+
+    /**
+     * Get the value that corresponds to being on the right edge of the
+     * horizontal scroll bar.
+     *
+     * @return the scroll value
+     */
+    public int getRightValue() {
+        if (hScroller == null) {
+            return 0;
+        } else {
+            return hScroller.getRightValue();
+        }
+    }
+
+    /**
+     * Set the value that corresponds to being on the right edge of the
+     * horizontal scroll bar.
+     *
+     * @param rightValue the new scroll value
+     */
+    public void setRightValue(final int rightValue) {
+        if (hScroller == null) {
+            return;
+        } else {
+            hScroller.setRightValue(rightValue);
+        }
+    }
+
+    /**
+     * Get current value of the horizontal scroll.
+     *
+     * @return the scroll value
+     */
+    public int getHorizontalValue() {
+        if (hScroller == null) {
+            return 0;
+        } else {
+            return hScroller.getValue();
+        }
+    }
+
+    /**
+     * Set current value of the horizontal scroll.
+     *
+     * @param value the new scroll value
+     */
+    public void setHorizontalValue(final int value) {
+        if (hScroller == null) {
+            return;
+        } else {
+            hScroller.setValue(value);
+        }
+    }
+
+    /**
+     * Get the increment for clicking on an arrow on the horizontal
+     * scrollbar.
+     *
+     * @return the increment value
+     */
+    public int getHorizontalSmallChange() {
+        if (hScroller == null) {
+            return 0;
+        } else {
+            return hScroller.getSmallChange();
+        }
+    }
+
+    /**
+     * Set the increment for clicking on an arrow on the horizontal
+     * scrollbar.
+     *
+     * @param smallChange the new increment value
+     */
+    public void setHorizontalSmallChange(final int smallChange) {
+        if (hScroller == null) {
+            return;
+        } else {
+            hScroller.setSmallChange(smallChange);
+        }
+    }
+
+    /**
+     * Get the increment for clicking in the bar between the box and an
+     * arrow on the horizontal scrollbar.
+     *
+     * @return the increment value
+     */
+    public int getHorizontalBigChange() {
+        if (hScroller == null) {
+            return 0;
+        } else {
+            return hScroller.getBigChange();
+        }
+    }
+
+    /**
+     * Set the increment for clicking in the bar between the box and an
+     * arrow on the horizontal scrollbar.
+     *
+     * @param bigChange the new increment value
+     */
+    public void setHorizontalBigChange(final int bigChange) {
+        if (hScroller == null) {
+            return;
+        } else {
+            hScroller.setBigChange(bigChange);
+        }
+    }
+
+    /**
+     * Perform a small step change left.
+     */
+    public void horizontalDecrement() {
+        if (hScroller == null) {
+            return;
+        } else {
+            hScroller.decrement();
+        }
+    }
+
+    /**
+     * Perform a small step change right.
+     */
+    public void horizontalIncrement() {
+        if (hScroller == null) {
+            return;
+        } else {
+            hScroller.increment();
+        }
+    }
+
+    /**
+     * Perform a big step change left.
+     */
+    public void bigHorizontalDecrement() {
+        if (hScroller == null) {
+            return;
+        } else {
+            hScroller.bigDecrement();
+        }
+    }
+
+    /**
+     * Perform a big step change right.
+     */
+    public void bigHorizontalIncrement() {
+        if (hScroller == null) {
+            return;
+        } else {
+            hScroller.bigIncrement();
+        }
+    }
+
+    /**
+     * Go to the left edge of the horizontal scroller.
+     */
+    public void toLeft() {
+        if (hScroller == null) {
+            return;
+        } else {
+            hScroller.toLeft();
+        }
+    }
+
+    /**
+     * Go to the right edge of the horizontal scroller.
+     */
+    public void toRight() {
+        if (hScroller == null) {
+            return;
+        } else {
+            hScroller.toRight();
+        }
+    }
+
+    /**
+     * Go to the top-left edge of the horizontal and vertical scrollers.
+     */
+    public void toHome() {
+        if (hScroller != null) {
+            hScroller.toLeft();
+        }
+        if (vScroller != null) {
+            vScroller.toTop();
+        }
+    }
+
+    /**
+     * Go to the bottom-right edge of the horizontal and vertical scrollers.
+     */
+    public void toEnd() {
+        if (hScroller != null) {
+            hScroller.toRight();
+        }
+        if (vScroller != null) {
+            vScroller.toBottom();
+        }
+    }
+
+}
diff --git a/src/jexer/TScrollableWindow.java b/src/jexer/TScrollableWindow.java
new file mode 100644 (file)
index 0000000..8935ac3
--- /dev/null
@@ -0,0 +1,602 @@
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2017 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 jexer.event.TResizeEvent;
+
+/**
+ * TScrollableWindow is a convenience superclass for windows that have
+ * scrollbars.
+ */
+public class TScrollableWindow extends TWindow implements Scrollable {
+
+    /**
+     * The horizontal scrollbar.
+     */
+    protected THScroller hScroller = null;
+
+    /**
+     * The vertical scrollbar.
+     */
+    protected TVScroller vScroller = null;
+
+    /**
+     * 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() - 2);
+            hScroller.setWidth(getWidth() - 3);
+            hScroller.setBigChange(getWidth() - 3);
+        }
+        if (vScroller != null) {
+            vScroller.setX(getWidth() - 2);
+            vScroller.setHeight(getHeight() - 2);
+            vScroller.setBigChange(getHeight() - 2);
+        }
+    }
+
+    /**
+     * Recompute whatever data is displayed by this widget.
+     */
+    public void reflowData() {
+        // Default: nothing to do
+    }
+
+    /**
+     * Handle window/screen resize events.
+     *
+     * @param event resize event
+     */
+    @Override
+    public void onResize(final TResizeEvent event) {
+        if (event.getType() == TResizeEvent.Type.WIDGET) {
+            reflowData();
+            placeScrollbars();
+            return;
+        } else {
+            super.onResize(event);
+        }
+    }
+
+    /**
+     * Public constructor.  Window will be located at (0, 0).
+     *
+     * @param application TApplication that manages this window
+     * @param title window title, will be centered along the top border
+     * @param width width of window
+     * @param height height of window
+     */
+    public TScrollableWindow(final TApplication application, final String title,
+        final int width, final int height) {
+
+        super(application, title, width, height);
+    }
+
+    /**
+     * Public constructor.  Window will be located at (0, 0).
+     *
+     * @param application TApplication that manages this window
+     * @param title window title, will be centered along the top border
+     * @param width width of window
+     * @param height height of window
+     * @param flags bitmask of RESIZABLE, CENTERED, or MODAL
+     */
+    public TScrollableWindow(final TApplication application, final String title,
+        final int width, final int height, final int flags) {
+
+        super(application, title, width, height, flags);
+    }
+
+    /**
+     * Public constructor.
+     *
+     * @param application TApplication that manages this window
+     * @param title window title, will be centered along the top border
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param width width of window
+     * @param height height of window
+     */
+    public TScrollableWindow(final TApplication application, final String title,
+        final int x, final int y, final int width, final int height) {
+
+        super(application, title, x, y, width, height);
+    }
+
+    /**
+     * Public constructor.
+     *
+     * @param application TApplication that manages this window
+     * @param title window title, will be centered along the top border
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param width width of window
+     * @param height height of window
+     * @param flags mask of RESIZABLE, CENTERED, or MODAL
+     */
+    public TScrollableWindow(final TApplication application, final String title,
+        final int x, final int y, final int width, final int height,
+        final int flags) {
+
+        super(application, title, x, y, width, height, flags);
+    }
+
+    /**
+     * Get the horizontal scrollbar, or null if this Viewport does not
+     * support horizontal scrolling.
+     *
+     * @return the horizontal scrollbar
+     */
+    public THScroller getHorizontalScroller() {
+        return hScroller;
+    }
+
+    /**
+     * Get the vertical scrollbar, or null if this Viewport does not support
+     * vertical scrolling.
+     *
+     * @return the vertical scrollbar
+     */
+    public TVScroller getVerticalScroller() {
+        return vScroller;
+    }
+
+    /**
+     * Get the value that corresponds to being on the top edge of the
+     * vertical scroll bar.
+     *
+     * @return the scroll value
+     */
+    public int getTopValue() {
+        if (vScroller == null) {
+            return 0;
+        } else {
+            return vScroller.getTopValue();
+        }
+    }
+
+    /**
+     * Set the value that corresponds to being on the top edge of the
+     * vertical scroll bar.
+     *
+     * @param topValue the new scroll value
+     */
+    public void setTopValue(final int topValue) {
+        if (vScroller == null) {
+            return;
+        } else {
+            vScroller.setTopValue(topValue);
+        }
+    }
+
+    /**
+     * Get the value that corresponds to being on the bottom edge of the
+     * vertical scroll bar.
+     *
+     * @return the scroll value
+     */
+    public int getBottomValue() {
+        if (vScroller == null) {
+            return 0;
+        } else {
+            return vScroller.getBottomValue();
+        }
+    }
+
+    /**
+     * Set the value that corresponds to being on the bottom edge of the
+     * vertical scroll bar.
+     *
+     * @param bottomValue the new scroll value
+     */
+    public void setBottomValue(final int bottomValue) {
+        if (vScroller == null) {
+            return;
+        } else {
+            vScroller.setBottomValue(bottomValue);
+        }
+    }
+
+    /**
+     * Get current value of the vertical scroll.
+     *
+     * @return the scroll value
+     */
+    public int getVerticalValue() {
+        if (vScroller == null) {
+            return 0;
+        } else {
+            return vScroller.getValue();
+        }
+    }
+
+    /**
+     * Set current value of the vertical scroll.
+     *
+     * @param value the new scroll value
+     */
+    public void setVerticalValue(final int value) {
+        if (vScroller == null) {
+            return;
+        } else {
+            vScroller.setValue(value);
+        }
+    }
+
+    /**
+     * Get the increment for clicking on an arrow on the vertical scrollbar.
+     *
+     * @return the increment value
+     */
+    public int getVerticalSmallChange() {
+        if (vScroller == null) {
+            return 0;
+        } else {
+            return vScroller.getSmallChange();
+        }
+    }
+
+    /**
+     * Set the increment for clicking on an arrow on the vertical scrollbar.
+     *
+     * @param smallChange the new increment value
+     */
+    public void setVerticalSmallChange(final int smallChange) {
+        if (vScroller == null) {
+            return;
+        } else {
+            vScroller.setSmallChange(smallChange);
+        }
+    }
+
+    /**
+     * Get the increment for clicking in the bar between the box and an
+     * arrow on the vertical scrollbar.
+     *
+     * @return the increment value
+     */
+    public int getVerticalBigChange() {
+        if (vScroller == null) {
+            return 0;
+        } else {
+            return vScroller.getBigChange();
+        }
+    }
+
+    /**
+     * Set the increment for clicking in the bar between the box and an
+     * arrow on the vertical scrollbar.
+     *
+     * @param bigChange the new increment value
+     */
+    public void setVerticalBigChange(final int bigChange) {
+        if (vScroller == null) {
+            return;
+        } else {
+            vScroller.setBigChange(bigChange);
+        }
+    }
+
+    /**
+     * Perform a small step change up.
+     */
+    public void verticalDecrement() {
+        if (vScroller == null) {
+            return;
+        } else {
+            vScroller.decrement();
+        }
+    }
+
+    /**
+     * Perform a small step change down.
+     */
+    public void verticalIncrement() {
+        if (vScroller == null) {
+            return;
+        } else {
+            vScroller.increment();
+        }
+    }
+
+    /**
+     * Perform a big step change up.
+     */
+    public void bigVerticalDecrement() {
+        if (vScroller == null) {
+            return;
+        } else {
+            vScroller.bigDecrement();
+        }
+    }
+
+    /**
+     * Perform a big step change down.
+     */
+    public void bigVerticalIncrement() {
+        if (vScroller == null) {
+            return;
+        } else {
+            vScroller.bigIncrement();
+        }
+    }
+
+    /**
+     * Go to the top edge of the vertical scroller.
+     */
+    public void toTop() {
+        if (vScroller == null) {
+            return;
+        } else {
+            vScroller.toTop();
+        }
+    }
+
+    /**
+     * Go to the bottom edge of the vertical scroller.
+     */
+    public void toBottom() {
+        if (vScroller == null) {
+            return;
+        } else {
+            vScroller.toBottom();
+        }
+    }
+
+    /**
+     * Get the value that corresponds to being on the left edge of the
+     * horizontal scroll bar.
+     *
+     * @return the scroll value
+     */
+    public int getLeftValue() {
+        if (hScroller == null) {
+            return 0;
+        } else {
+            return hScroller.getLeftValue();
+        }
+    }
+
+    /**
+     * Set the value that corresponds to being on the left edge of the
+     * horizontal scroll bar.
+     *
+     * @param leftValue the new scroll value
+     */
+    public void setLeftValue(final int leftValue) {
+        if (hScroller == null) {
+            return;
+        } else {
+            hScroller.setLeftValue(leftValue);
+        }
+    }
+
+    /**
+     * Get the value that corresponds to being on the right edge of the
+     * horizontal scroll bar.
+     *
+     * @return the scroll value
+     */
+    public int getRightValue() {
+        if (hScroller == null) {
+            return 0;
+        } else {
+            return hScroller.getRightValue();
+        }
+    }
+
+    /**
+     * Set the value that corresponds to being on the right edge of the
+     * horizontal scroll bar.
+     *
+     * @param rightValue the new scroll value
+     */
+    public void setRightValue(final int rightValue) {
+        if (hScroller == null) {
+            return;
+        } else {
+            hScroller.setRightValue(rightValue);
+        }
+    }
+
+    /**
+     * Get current value of the horizontal scroll.
+     *
+     * @return the scroll value
+     */
+    public int getHorizontalValue() {
+        if (hScroller == null) {
+            return 0;
+        } else {
+            return hScroller.getValue();
+        }
+    }
+
+    /**
+     * Set current value of the horizontal scroll.
+     *
+     * @param value the new scroll value
+     */
+    public void setHorizontalValue(final int value) {
+        if (hScroller == null) {
+            return;
+        } else {
+            hScroller.setValue(value);
+        }
+    }
+
+    /**
+     * Get the increment for clicking on an arrow on the horizontal
+     * scrollbar.
+     *
+     * @return the increment value
+     */
+    public int getHorizontalSmallChange() {
+        if (hScroller == null) {
+            return 0;
+        } else {
+            return hScroller.getSmallChange();
+        }
+    }
+
+    /**
+     * Set the increment for clicking on an arrow on the horizontal
+     * scrollbar.
+     *
+     * @param smallChange the new increment value
+     */
+    public void setHorizontalSmallChange(final int smallChange) {
+        if (hScroller == null) {
+            return;
+        } else {
+            hScroller.setSmallChange(smallChange);
+        }
+    }
+
+    /**
+     * Get the increment for clicking in the bar between the box and an
+     * arrow on the horizontal scrollbar.
+     *
+     * @return the increment value
+     */
+    public int getHorizontalBigChange() {
+        if (hScroller == null) {
+            return 0;
+        } else {
+            return hScroller.getBigChange();
+        }
+    }
+
+    /**
+     * Set the increment for clicking in the bar between the box and an
+     * arrow on the horizontal scrollbar.
+     *
+     * @param bigChange the new increment value
+     */
+    public void setHorizontalBigChange(final int bigChange) {
+        if (hScroller == null) {
+            return;
+        } else {
+            hScroller.setBigChange(bigChange);
+        }
+    }
+
+    /**
+     * Perform a small step change left.
+     */
+    public void horizontalDecrement() {
+        if (hScroller == null) {
+            return;
+        } else {
+            hScroller.decrement();
+        }
+    }
+
+    /**
+     * Perform a small step change right.
+     */
+    public void horizontalIncrement() {
+        if (hScroller == null) {
+            return;
+        } else {
+            hScroller.increment();
+        }
+    }
+
+    /**
+     * Perform a big step change left.
+     */
+    public void bigHorizontalDecrement() {
+        if (hScroller == null) {
+            return;
+        } else {
+            hScroller.bigDecrement();
+        }
+    }
+
+    /**
+     * Perform a big step change right.
+     */
+    public void bigHorizontalIncrement() {
+        if (hScroller == null) {
+            return;
+        } else {
+            hScroller.bigIncrement();
+        }
+    }
+
+    /**
+     * Go to the left edge of the horizontal scroller.
+     */
+    public void toLeft() {
+        if (hScroller == null) {
+            return;
+        } else {
+            hScroller.toLeft();
+        }
+    }
+
+    /**
+     * Go to the right edge of the horizontal scroller.
+     */
+    public void toRight() {
+        if (hScroller == null) {
+            return;
+        } else {
+            hScroller.toRight();
+        }
+    }
+
+    /**
+     * Go to the top-left edge of the horizontal and vertical scrollers.
+     */
+    public void toHome() {
+        if (hScroller != null) {
+            hScroller.toLeft();
+        }
+        if (vScroller != null) {
+            vScroller.toTop();
+        }
+    }
+
+    /**
+     * Go to the bottom-right edge of the horizontal and vertical scrollers.
+     */
+    public void toEnd() {
+        if (hScroller != null) {
+            hScroller.toRight();
+        }
+        if (vScroller != null) {
+            vScroller.toBottom();
+        }
+    }
+
+}
index 41c4b5eca784aaa051f7b5566cb6262fd1a8532d..17f32e0772db527b54fadc59378263a4e517f340 100644 (file)
@@ -49,7 +49,7 @@ import static jexer.TKeypress.*;
 /**
  * TTerminalWindow exposes a ECMA-48 / ANSI X3.64 style terminal in a window.
  */
-public class TTerminalWindow extends TWindow {
+public class TTerminalWindow extends TScrollableWindow {
 
     /**
      * The emulator.
@@ -61,11 +61,6 @@ public class TTerminalWindow extends TWindow {
      */
     private Process shell;
 
-    /**
-     * Vertical scrollbar.
-     */
-    private TVScroller vScroller;
-
     /**
      * Claim the keystrokes the emulator will need.
      */
@@ -149,6 +144,9 @@ public class TTerminalWindow extends TWindow {
 
         super(application, "Terminal", x, y, 80 + 2, 24 + 2, flags);
 
+        vScroller = new TVScroller(this, getWidth() - 2, 0, getHeight() - 2);
+        setBottomValue(0);
+
         // Assume XTERM
         ECMA48.DeviceType deviceType = ECMA48.DeviceType.XTERM;
 
@@ -281,7 +279,7 @@ public class TTerminalWindow extends TWindow {
         synchronized (emulator) {
 
             // Update the scroll bars
-            reflow();
+            reflowData();
 
             // Draw the box using my superclass
             super.draw();
@@ -292,7 +290,7 @@ public class TTerminalWindow extends TWindow {
             // Put together the visible rows
             int visibleHeight = getHeight() - 2;
             int visibleBottom = scrollback.size() + display.size()
-                + vScroller.getValue();
+                + getVerticalValue();
             assert (visibleBottom >= 0);
 
             List<DisplayLine> preceedingBlankLines = new LinkedList<DisplayLine>();
@@ -385,10 +383,8 @@ public class TTerminalWindow extends TWindow {
 
             setCursorX(emulator.getCursorX() + 1);
             setCursorY(emulator.getCursorY() + 1
-                + (getHeight() - 2 - emulator.getHeight()));
-            if (vScroller != null) {
-                setCursorY(getCursorY() - vScroller.getValue());
-            }
+                + (getHeight() - 2 - emulator.getHeight())
+                - getVerticalValue());
             setCursorVisible(emulator.isCursorVisible());
             if (getCursorX() > getWidth() - 2) {
                 setCursorVisible(false);
@@ -454,10 +450,11 @@ public class TTerminalWindow extends TWindow {
 
             if (resize.getType() == TResizeEvent.Type.WIDGET) {
                 // Resize the scroll bars
-                reflow();
+                reflowData();
+                placeScrollbars();
 
                 // Get out of scrollback
-                vScroller.setValue(0);
+                setVerticalValue(0);
             }
             return;
 
@@ -467,7 +464,8 @@ public class TTerminalWindow extends TWindow {
     /**
      * Resize scrollbars for a new width/height.
      */
-    private void reflow() {
+    @Override
+    public void reflowData() {
 
         // Synchronize against the emulator so we don't stomp on its reader
         // thread.
@@ -477,19 +475,10 @@ public class TTerminalWindow extends TWindow {
             readEmulatorState();
 
             // Vertical scrollbar
-            if (vScroller == null) {
-                vScroller = new TVScroller(this, getWidth() - 2, 0,
-                    getHeight() - 2);
-                vScroller.setBottomValue(0);
-                vScroller.setValue(0);
-            } else {
-                vScroller.setX(getWidth() - 2);
-                vScroller.setHeight(getHeight() - 2);
-            }
-            vScroller.setTopValue(getHeight() - 2
+            setTopValue(getHeight() - 2
                 - (emulator.getScrollbackBuffer().size()
                     + emulator.getDisplayBuffer().size()));
-            vScroller.setBigChange(getHeight() - 2);
+            setVerticalBigChange(getHeight() - 2);
 
         } // synchronized (emulator)
     }
@@ -532,14 +521,14 @@ public class TTerminalWindow extends TWindow {
             || keypress.equals(kbCtrlPgUp)
             || keypress.equals(kbAltPgUp)
         ) {
-            vScroller.bigDecrement();
+            bigVerticalDecrement();
             return;
         }
         if (keypress.equals(kbShiftPgDn)
             || keypress.equals(kbCtrlPgDn)
             || keypress.equals(kbAltPgDn)
         ) {
-            vScroller.bigIncrement();
+            bigVerticalIncrement();
             return;
         }
 
@@ -548,7 +537,7 @@ public class TTerminalWindow extends TWindow {
         synchronized (emulator) {
             if (emulator.isReading()) {
                 // Get out of scrollback
-                vScroller.setValue(0);
+                setVerticalValue(0);
                 emulator.keypress(keypress.getKey());
 
                 // UGLY HACK TIME!  cmd.exe needs CRLF, not just CR, so if
@@ -582,11 +571,11 @@ public class TTerminalWindow extends TWindow {
         }
 
         if (mouse.isMouseWheelUp()) {
-            vScroller.decrement();
+            verticalDecrement();
             return;
         }
         if (mouse.isMouseWheelDown()) {
-            vScroller.increment();
+            verticalIncrement();
             return;
         }
         if (mouseOnEmulator(mouse)) {
index 21c2f1f1445ed826106061bb47e25e14c3251664..c57e8846ace8b8ee160d250147333bd63723ab76 100644 (file)
@@ -48,7 +48,7 @@ import jexer.event.TMouseEvent;
  * TText implements a simple scrollable text area. It reflows automatically on
  * resize.
  */
-public final class TText extends TWidget {
+public final class TText extends TScrollableWidget {
 
     /**
      * Available text justifications.
@@ -96,24 +96,33 @@ public final class TText extends TWidget {
     private String colorKey;
 
     /**
-     * Vertical scrollbar.
+     * Maximum width of a single line.
      */
-    private TVScroller vScroller;
+    private int maxLineWidth;
 
     /**
-     * Horizontal scrollbar.
+     * Number of lines between each paragraph.
      */
-    private THScroller hScroller;
+    private int lineSpacing = 1;
 
     /**
-     * Maximum width of a single line.
+     * Set the text.
+     *
+     * @param text new text to display
      */
-    private int maxLineWidth;
+    public void setText(final String text) {
+        this.text = text;
+        reflowData();
+    }
 
     /**
-     * Number of lines between each paragraph.
+     * Get the text.
+     *
+     * @return the text
      */
-    private int lineSpacing = 1;
+    public String getText() {
+        return text;
+    }
 
     /**
      * Convenience method used by TWindowLoggerOutput.
@@ -127,7 +136,7 @@ public final class TText extends TWidget {
             text += "\n\n";
             text += line;
         }
-        reflow();
+        reflowData();
     }
 
     /**
@@ -141,6 +150,7 @@ public final class TText extends TWidget {
             }
         }
 
+        vScroller.setTopValue(0);
         vScroller.setBottomValue((lines.size() - getHeight()) + 1);
         if (vScroller.getBottomValue() < 0) {
             vScroller.setBottomValue(0);
@@ -149,6 +159,7 @@ public final class TText extends TWidget {
             vScroller.setValue(vScroller.getBottomValue());
         }
 
+        hScroller.setLeftValue(0);
         hScroller.setRightValue((maxLineWidth - getWidth()) + 1);
         if (hScroller.getRightValue() < 0) {
             hScroller.setRightValue(0);
@@ -165,7 +176,7 @@ public final class TText extends TWidget {
      */
     public void setJustification(final Justification justification) {
         this.justification = justification;
-        reflow();
+        reflowData();
     }
 
     /**
@@ -173,7 +184,7 @@ public final class TText extends TWidget {
      */
     public void leftJustify() {
         justification = Justification.LEFT;
-        reflow();
+        reflowData();
     }
 
     /**
@@ -181,7 +192,7 @@ public final class TText extends TWidget {
      */
     public void centerJustify() {
         justification = Justification.CENTER;
-        reflow();
+        reflowData();
     }
 
     /**
@@ -189,7 +200,7 @@ public final class TText extends TWidget {
      */
     public void rightJustify() {
         justification = Justification.RIGHT;
-        reflow();
+        reflowData();
     }
 
     /**
@@ -197,13 +208,14 @@ public final class TText extends TWidget {
      */
     public void fullJustify() {
         justification = Justification.FULL;
-        reflow();
+        reflowData();
     }
 
     /**
      * Resize text and scrollbars for a new width/height.
      */
-    public void reflow() {
+    @Override
+    public void reflowData() {
         // Reset the lines
         lines.clear();
 
@@ -233,29 +245,6 @@ public final class TText extends TWidget {
                 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();
     }
 
@@ -299,7 +288,9 @@ public final class TText extends TWidget {
 
         lines = new LinkedList<String>();
 
-        reflow();
+        vScroller = new TVScroller(this, getWidth() - 1, 0, getHeight() - 1);
+        hScroller = new THScroller(this, 0, getHeight() - 1, getWidth() - 1);
+        reflowData();
     }
 
     /**
index cc48ffa6814641905c4c8354dd305c0b0e22137f..7de5e123587fe16aa06a62ab5737f27afa5d41ff 100644 (file)
@@ -232,7 +232,7 @@ public class TTreeItem extends TWidget {
             view.setTreeRoot(this, true);
         }
 
-        view.reflow();
+        view.reflowData();
     }
 
     /**
@@ -256,7 +256,7 @@ public class TTreeItem extends TWidget {
         TTreeItem item = new TTreeItem(view, text, expanded);
         item.level = this.level + 1;
         getChildren().add(item);
-        view.reflow();
+        view.reflowData();
         return item;
     }
 
@@ -333,7 +333,7 @@ public class TTreeItem extends TWidget {
      */
     @Override
     public void onMouseUp(final TMouseEvent mouse) {
-        if ((mouse.getX() == (getExpanderX() - view.getHScroller().getValue()))
+        if ((mouse.getX() == (getExpanderX() - view.getHorizontalValue()))
             && (mouse.getY() == 0)
         ) {
             if (selectable) {
@@ -352,7 +352,7 @@ public class TTreeItem extends TWidget {
         }
 
         // Update the screen after any thing has expanded/contracted
-        view.reflow();
+        view.reflowData();
     }
 
     /**
@@ -403,7 +403,7 @@ public class TTreeItem extends TWidget {
             return;
         }
 
-        int offset = -view.getHScroller().getValue();
+        int offset = -view.getHorizontalValue();
 
         CellAttributes color = getTheme().getColor("ttreeview");
         CellAttributes textColor = getTheme().getColor("ttreeview");
index 6dc4f161fdfdd6db3249575549e35e4cacd94101..b9b05bf0fd492dc0d5b83933a0736d52d7720c55 100644 (file)
@@ -35,27 +35,7 @@ import static jexer.TKeypress.*;
 /**
  * TTreeView implements a simple tree view.
  */
-public class TTreeView extends TWidget {
-
-    /**
-     * Vertical scrollbar.
-     */
-    private TVScroller vScroller;
-
-    /**
-     * Horizontal scrollbar.
-     */
-    private THScroller hScroller;
-
-    /**
-     * Get the horizontal scrollbar.  This is used by TTreeItem.draw(), and
-     * potentially subclasses.
-     *
-     * @return the horizontal scrollbar
-     */
-    public final THScroller getHScroller() {
-        return hScroller;
-    }
+public class TTreeView extends TScrollableWidget {
 
     /**
      * Root of the tree.
@@ -144,6 +124,9 @@ public class TTreeView extends TWidget {
 
         super(parent, x, y, width, height);
         this.action = action;
+
+        vScroller = new TVScroller(this, getWidth() - 1, 0, getHeight() - 1);
+        hScroller = new THScroller(this, 0, getHeight() - 1, getWidth() - 1);
     }
 
     /**
@@ -179,41 +162,14 @@ public class TTreeView extends TWidget {
         }
     }
 
-    /**
-     * Update (or instantiate) vScroller and hScroller.
-     */
-    private void updateScrollers() {
-        // Setup vertical scroller
-        if (vScroller == null) {
-            vScroller = new TVScroller(this, getWidth() - 1, 0,
-                getHeight() - 1);
-            vScroller.setValue(0);
-            vScroller.setTopValue(0);
-        }
-        vScroller.setX(getWidth() - 1);
-        vScroller.setHeight(getHeight() - 1);
-        vScroller.setBigChange(getHeight() - 1);
-
-        // Setup horizontal scroller
-        if (hScroller == null) {
-            hScroller = new THScroller(this, 0, getHeight() - 1,
-                getWidth() - 1);
-            hScroller.setValue(0);
-            hScroller.setLeftValue(0);
-        }
-        hScroller.setY(getHeight() - 1);
-        hScroller.setWidth(getWidth() - 1);
-        hScroller.setBigChange(getWidth() - 1);
-    }
-
     /**
      * Resize text and scrollbars for a new width/height.
      */
-    public void reflow() {
+    @Override
+    public void reflowData() {
         int selectedRow = 0;
         boolean foundSelectedRow = false;
 
-        updateScrollers();
         if (treeRoot == null) {
             return;
         }
@@ -253,34 +209,30 @@ public class TTreeView extends TWidget {
         }
 
         if ((centerWindow) && (foundSelectedRow)) {
-            if ((selectedRow < vScroller.getValue())
-                || (selectedRow > vScroller.getValue() + getHeight() - 2)
+            if ((selectedRow < getVerticalValue())
+                || (selectedRow > getVerticalValue() + getHeight() - 2)
             ) {
-                vScroller.setValue(selectedRow);
+                setVerticalValue(selectedRow);
                 centerWindow = false;
             }
         }
         updatePositions();
 
         // Rescale the scroll bars
-        vScroller.setBottomValue(getChildren().size() - getHeight() + 1);
-        if (vScroller.getBottomValue() < 0) {
-            vScroller.setBottomValue(0);
+        setBottomValue(getChildren().size() - getHeight() + 1);
+        if (getBottomValue() < 0) {
+            setBottomValue(0);
         }
-        /*
-        if (vScroller.getValue() > vScroller.getBottomValue()) {
-            vScroller.setValue(vScroller.getBottomValue());
+        if (getVerticalValue() > getBottomValue()) {
+            setVerticalValue(getBottomValue());
         }
-         */
-        hScroller.setRightValue(maxLineWidth - getWidth() + 3);
-        if (hScroller.getRightValue() < 0) {
-            hScroller.setRightValue(0);
+        setRightValue(maxLineWidth - getWidth() + 3);
+        if (getRightValue() < 0) {
+            setRightValue(0);
         }
-        /*
-        if (hScroller.getValue() > hScroller.getRightValue()) {
-            hScroller.setValue(hScroller.getRightValue());
+        if (getHorizontalValue() > getRightValue()) {
+            setHorizontalValue(getRightValue());
         }
-         */
         getChildren().add(hScroller);
         getChildren().add(vScroller);
     }
@@ -293,7 +245,7 @@ public class TTreeView extends TWidget {
             return;
         }
 
-        int begin = vScroller.getValue();
+        int begin = getVerticalValue();
         int topY = 0;
 
         // As we walk the list we also adjust next/previous pointers,
@@ -344,16 +296,16 @@ public class TTreeView extends TWidget {
     @Override
     public void onMouseDown(final TMouseEvent mouse) {
         if (mouse.isMouseWheelUp()) {
-            vScroller.decrement();
+            verticalDecrement();
         } else if (mouse.isMouseWheelDown()) {
-            vScroller.increment();
+            verticalIncrement();
         } else {
             // Pass to children
             super.onMouseDown(mouse);
         }
 
         // Update the screen after the scrollbars have moved
-        reflow();
+        reflowData();
     }
 
     /**
@@ -367,7 +319,7 @@ public class TTreeView extends TWidget {
         super.onMouseDown(mouse);
 
         // Update the screen after any thing has expanded/contracted
-        reflow();
+        reflowData();
     }
 
     /**
@@ -381,36 +333,36 @@ public class TTreeView extends TWidget {
             || keypress.equals(kbCtrlLeft)
             || keypress.equals(kbAltLeft)
         ) {
-            hScroller.decrement();
+            horizontalDecrement();
         } else if (keypress.equals(kbShiftRight)
             || keypress.equals(kbCtrlRight)
             || keypress.equals(kbAltRight)
         ) {
-            hScroller.increment();
+            horizontalIncrement();
         } else if (keypress.equals(kbShiftUp)
             || keypress.equals(kbCtrlUp)
             || keypress.equals(kbAltUp)
         ) {
-            vScroller.decrement();
+            verticalDecrement();
         } else if (keypress.equals(kbShiftDown)
             || keypress.equals(kbCtrlDown)
             || keypress.equals(kbAltDown)
         ) {
-            vScroller.increment();
+            verticalIncrement();
         } else if (keypress.equals(kbShiftPgUp)
             || keypress.equals(kbCtrlPgUp)
             || keypress.equals(kbAltPgUp)
         ) {
-            vScroller.bigDecrement();
+            bigVerticalDecrement();
         } else if (keypress.equals(kbShiftPgDn)
             || keypress.equals(kbCtrlPgDn)
             || keypress.equals(kbAltPgDn)
         ) {
-            vScroller.bigIncrement();
+            bigVerticalIncrement();
         } else if (keypress.equals(kbHome)) {
-            vScroller.toTop();
+            toTop();
         } else if (keypress.equals(kbEnd)) {
-            vScroller.toBottom();
+            toBottom();
         } else if (keypress.equals(kbEnter)) {
             if (selectedItem != null) {
                 dispatch();
@@ -422,7 +374,7 @@ public class TTreeView extends TWidget {
                 if (selectedItem.keyboardPrevious != null) {
                     setSelected(selectedItem.keyboardPrevious);
                     if (oldItem.getY() == 0) {
-                        vScroller.decrement();
+                        verticalDecrement();
                     }
                 }
             }
@@ -433,7 +385,7 @@ public class TTreeView extends TWidget {
                 if (selectedItem.keyboardNext != null) {
                     setSelected(selectedItem.keyboardNext);
                     if (oldItem.getY() == getHeight() - 2) {
-                        vScroller.increment();
+                        verticalIncrement();
                     }
                 }
             }
@@ -454,7 +406,7 @@ public class TTreeView extends TWidget {
         }
 
         // Update the screen after any thing has expanded/contracted
-        reflow();
+        reflowData();
     }
 
 }
index 32e173ab6b5ab080c32b375b99d52d9f9465e4e9..18178807fc10d14e86cfcdfc9c3e385de102c0cf 100644 (file)
@@ -115,6 +115,15 @@ public final class TVScroller extends TWidget {
      */
     private int smallChange = 1;
 
+    /**
+     * Get the increment for clicking on an arrow.
+     *
+     * @return the increment value
+     */
+    public int getSmallChange() {
+        return smallChange;
+    }
+
     /**
      * Set the increment for clicking on an arrow.
      *
@@ -129,6 +138,16 @@ public final class TVScroller extends TWidget {
      */
     private int bigChange = 20;
 
+    /**
+     * Set the increment for clicking in the bar between the box and an
+     * arrow.
+     *
+     * @return the increment value
+     */
+    public int getBigChange() {
+        return bigChange;
+    }
+
     /**
      * Set the increment for clicking in the bar between the box and an
      * arrow.
index 1c51296af5900b14f010433278f16eb660566742..3538905d2a6cb1dca94998d3040e313965aa5acf 100644 (file)
@@ -120,9 +120,9 @@ public class DemoTextWindow extends TWindow {
     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() - 6);
-            textField.reflow();
+            TResizeEvent textSize = new TResizeEvent(TResizeEvent.Type.WIDGET,
+                event.getWidth() - 4, event.getHeight() - 6);
+            textField.onResize(textSize);
             return;
         }
 
index 365cbcac8bcf05151713a72e856a5972b3649a26..7697981c015bb8b0db59874b0091ee3697ba8332 100644 (file)
@@ -73,10 +73,10 @@ public class DemoTreeViewWindow extends TWindow {
     @Override
     public void onResize(final TResizeEvent resize) {
         if (resize.getType() == TResizeEvent.Type.WIDGET) {
-            // Resize the text field
-            treeView.setWidth(resize.getWidth() - 4);
-            treeView.setHeight(resize.getHeight() - 4);
-            treeView.reflow();
+            // Resize the treeView field
+            TResizeEvent treeSize = new TResizeEvent(TResizeEvent.Type.WIDGET,
+                resize.getWidth() - 4, resize.getHeight() - 4);
+            treeView.onResize(treeSize);
             return;
         }