only keep the (re)sources here
[fanfix.git] / src / jexer / TTableWidget.java
diff --git a/src/jexer/TTableWidget.java b/src/jexer/TTableWidget.java
deleted file mode 100644 (file)
index 9b4d7c9..0000000
+++ /dev/null
@@ -1,2361 +0,0 @@
-/*
- * Jexer - Java Text User Interface
- *
- * The MIT License (MIT)
- *
- * Copyright (C) 2019 Kevin Lamonte
- *
- * Permission is hereby granted, free of charge, to any person obtaining a
- * copy of this software and associated documentation files (the "Software"),
- * to deal in the Software without restriction, including without limitation
- * the rights to use, copy, modify, merge, publish, distribute, sublicense,
- * and/or sell copies of the Software, and to permit persons to whom the
- * Software is furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
- * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
- * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
- * DEALINGS IN THE SOFTWARE.
- *
- * @author Kevin Lamonte [kevin.lamonte@gmail.com]
- * @version 1
- */
-package jexer;
-
-import java.io.BufferedReader;
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.FileReader;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-
-import jexer.bits.CellAttributes;
-import jexer.bits.StringUtils;
-import jexer.event.TKeypressEvent;
-import jexer.event.TMouseEvent;
-import jexer.event.TResizeEvent;
-import static jexer.TKeypress.*;
-
-/**
- * TTableWidget is used to display and edit regular two-dimensional tables of
- * cells.
- *
- * This class was inspired by a TTable implementation originally developed by
- * David "Niki" ROULET [niki@nikiroo.be], made available under MIT at
- * https://github.com/nikiroo/jexer/tree/ttable_pull.
- */
-public class TTableWidget extends TWidget {
-
-    // ------------------------------------------------------------------------
-    // Constants --------------------------------------------------------------
-    // ------------------------------------------------------------------------
-
-    /**
-     * Available borders for cells.
-     */
-    public enum Border {
-        /**
-         * No border.
-         */
-        NONE,
-
-        /**
-         * Single bar: \u2502 (vertical) and \u2500 (horizontal).
-         */
-        SINGLE,
-
-        /**
-         * Double bar: \u2551 (vertical) and \u2550 (horizontal).
-         */
-        DOUBLE,
-
-        /**
-         * Thick bar: \u2503 (vertical heavy) and \u2501 (horizontal heavy).
-         */
-        THICK,
-    }
-
-    /**
-     * If true, put a grid of numbers in the cells.
-     */
-    private static final boolean DEBUG = false;
-
-    /**
-     * Row label width.
-     */
-    private static final int ROW_LABEL_WIDTH = 8;
-
-    /**
-     * Column label height.
-     */
-    private static final int COLUMN_LABEL_HEIGHT = 1;
-
-    /**
-     * Column default width.
-     */
-    private static final int COLUMN_DEFAULT_WIDTH = 8;
-
-    /**
-     * Extra rows to add.
-     */
-    private static final int EXTRA_ROWS = (DEBUG ? 10 : 0);
-
-    /**
-     * Extra columns to add.
-     */
-    private static final int EXTRA_COLUMNS = (DEBUG ? 3 : 0);
-
-    // ------------------------------------------------------------------------
-    // Variables --------------------------------------------------------------
-    // ------------------------------------------------------------------------
-
-    /**
-     * The underlying data, organized as columns.
-     */
-    private ArrayList<Column> columns = new ArrayList<Column>();
-
-    /**
-     * The underlying data, organized as rows.
-     */
-    private ArrayList<Row> rows = new ArrayList<Row>();
-
-    /**
-     * The row in model corresponding to the top-left visible cell.
-     */
-    private int top = 0;
-
-    /**
-     * The column in model corresponding to the top-left visible cell.
-     */
-    private int left = 0;
-
-    /**
-     * The row in model corresponding to the currently selected cell.
-     */
-    private int selectedRow = 0;
-
-    /**
-     * The column in model corresponding to the currently selected cell.
-     */
-    private int selectedColumn = 0;
-
-    /**
-     * If true, highlight the entire row of the currently-selected cell.
-     */
-    private boolean highlightRow = false;
-
-    /**
-     * If true, highlight the entire column of the currently-selected cell.
-     */
-    private boolean highlightColumn = false;
-
-    /**
-     * If true, show the row labels as the first column.
-     */
-    private boolean showRowLabels = true;
-
-    /**
-     * If true, show the column labels as the first row.
-     */
-    private boolean showColumnLabels = true;
-
-    /**
-     * The top border for the first row.
-     */
-    private Border topBorder = Border.NONE;
-
-    /**
-     * The left border for the first column.
-     */
-    private Border leftBorder = Border.NONE;
-
-    /**
-     * Column represents a column of cells.
-     */
-    public class Column {
-
-        /**
-         * X position of this column.
-         */
-        private int x = 0;
-
-        /**
-         * Width of column.
-         */
-        private int width = COLUMN_DEFAULT_WIDTH;
-
-        /**
-         * The cells of this column.
-         */
-        private ArrayList<Cell> cells = new ArrayList<Cell>();
-
-        /**
-         * Column label.
-         */
-        private String label = "";
-
-        /**
-         * The right border for this column.
-         */
-        private Border rightBorder = Border.NONE;
-
-        /**
-         * Constructor sets label to lettered column.
-         *
-         * @param col column number to use for this column.  Column 0 will be
-         * "A", column 1 will be "B", column 26 will be "AA", and so on.
-         */
-        Column(int col) {
-            label = makeColumnLabel(col);
-        }
-
-        /**
-         * Add an entry to this column.
-         *
-         * @param cell the cell to add
-         */
-        public void add(final Cell cell) {
-            cells.add(cell);
-        }
-
-        /**
-         * Get an entry from this column.
-         *
-         * @param row the entry index to get
-         * @return the cell at row
-         */
-        public Cell get(final int row) {
-            return cells.get(row);
-        }
-
-        /**
-         * Get the X position of the cells in this column.
-         *
-         * @return the position
-         */
-        public int getX() {
-            return x;
-        }
-
-        /**
-         * Set the X position of the cells in this column.
-         *
-         * @param x the position
-         */
-        public void setX(final int x) {
-            for (Cell cell: cells) {
-                cell.setX(x);
-            }
-            this.x = x;
-        }
-
-    }
-
-    /**
-     * Row represents a row of cells.
-     */
-    public class Row {
-
-        /**
-         * Y position of this row.
-         */
-        private int y = 0;
-
-        /**
-         * Height of row.
-         */
-        private int height = 1;
-
-        /**
-         * The cells of this row.
-         */
-        private ArrayList<Cell> cells = new ArrayList<Cell>();
-
-        /**
-         * Row label.
-         */
-        private String label = "";
-
-        /**
-         * The bottom border for this row.
-         */
-        private Border bottomBorder = Border.NONE;
-
-        /**
-         * Constructor sets label to numbered row.
-         *
-         * @param row row number to use for this row
-         */
-        Row(final int row) {
-            label = Integer.toString(row);
-        }
-
-        /**
-         * Add an entry to this column.
-         *
-         * @param cell the cell to add
-         */
-        public void add(final Cell cell) {
-            cells.add(cell);
-        }
-
-        /**
-         * Get an entry from this row.
-         *
-         * @param column the entry index to get
-         * @return the cell at column
-         */
-        public Cell get(final int column) {
-            return cells.get(column);
-        }
-        /**
-         * Get the Y position of the cells in this column.
-         *
-         * @return the position
-         */
-        public int getY() {
-            return y;
-        }
-
-        /**
-         * Set the Y position of the cells in this column.
-         *
-         * @param y the position
-         */
-        public void setY(final int y) {
-            for (Cell cell: cells) {
-                cell.setY(y);
-            }
-            this.y = y;
-        }
-
-    }
-
-    /**
-     * Cell represents an editable cell in the table.  Normally, navigation
-     * to a cell only highlights it; pressing Enter or F2 will switch to
-     * editing mode.
-     */
-    public class Cell extends TWidget {
-
-        // --------------------------------------------------------------------
-        // Variables ----------------------------------------------------------
-        // --------------------------------------------------------------------
-
-        /**
-         * The field containing the cell's data.
-         */
-        private TField field;
-
-        /**
-         * The column of this cell.
-         */
-        private int column;
-
-        /**
-         * The row of this cell.
-         */
-        private int row;
-
-        /**
-         * If true, the cell is being edited.
-         */
-        private boolean isEditing = false;
-
-        /**
-         * If true, the cell is read-only (non-editable).
-         */
-        private boolean readOnly = false;
-
-        /**
-         * Text of field before editing.
-         */
-        private String fieldText;
-
-        // --------------------------------------------------------------------
-        // Constructors -------------------------------------------------------
-        // --------------------------------------------------------------------
-
-        /**
-         * Public 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
-         * @param column column index of this cell
-         * @param row row index of this cell
-         */
-        public Cell(final TTableWidget parent, final int x, final int y,
-            final int width, final int height, final int column,
-            final int row) {
-
-            super(parent, x, y, width, height);
-            this.column = column;
-            this.row = row;
-
-            field = addField(0, 0, width, false);
-            field.setEnabled(false);
-            field.setBackgroundChar(' ');
-        }
-
-        // --------------------------------------------------------------------
-        // Event handlers -----------------------------------------------------
-        // --------------------------------------------------------------------
-
-        /**
-         * Handle mouse double-click events.
-         *
-         * @param mouse mouse double-click event
-         */
-        @Override
-        public void onMouseDoubleClick(final TMouseEvent mouse) {
-            // Use TWidget's code to pass the event to the children.
-            super.onMouseDown(mouse);
-
-            // Double-click means to start editing.
-            fieldText = field.getText();
-            isEditing = true;
-            field.setEnabled(true);
-            activate(field);
-
-            if (isActive()) {
-                // Let the table know that I was activated.
-                ((TTableWidget) getParent()).selectedRow = row;
-                ((TTableWidget) getParent()).selectedColumn = column;
-                ((TTableWidget) getParent()).alignGrid();
-            }
-        }
-
-        /**
-         * Handle mouse press events.
-         *
-         * @param mouse mouse button press event
-         */
-        @Override
-        public void onMouseDown(final TMouseEvent mouse) {
-            // Use TWidget's code to pass the event to the children.
-            super.onMouseDown(mouse);
-
-            if (isActive()) {
-                // Let the table know that I was activated.
-                ((TTableWidget) getParent()).selectedRow = row;
-                ((TTableWidget) getParent()).selectedColumn = column;
-                ((TTableWidget) getParent()).alignGrid();
-            }
-        }
-
-        /**
-         * Handle mouse release events.
-         *
-         * @param mouse mouse button release event
-         */
-        @Override
-        public void onMouseUp(final TMouseEvent mouse) {
-            // Use TWidget's code to pass the event to the children.
-            super.onMouseDown(mouse);
-
-            if (isActive()) {
-                // Let the table know that I was activated.
-                ((TTableWidget) getParent()).selectedRow = row;
-                ((TTableWidget) getParent()).selectedColumn = column;
-                ((TTableWidget) getParent()).alignGrid();
-            }
-        }
-
-        /**
-         * Handle keystrokes.
-         *
-         * @param keypress keystroke event
-         */
-        @Override
-        public void onKeypress(final TKeypressEvent keypress) {
-            // System.err.println("Cell onKeypress: " + keypress);
-
-            if (readOnly) {
-                // Read only: do nothing.
-                return;
-            }
-
-            if (isEditing) {
-                if (keypress.equals(kbEsc)) {
-                    // ESC cancels the edit.
-                    cancelEdit();
-                    return;
-                }
-                if (keypress.equals(kbEnter)) {
-                    // Enter ends editing.
-
-                    // Pass down to field first so that it can execute
-                    // enterAction if specified.
-                    super.onKeypress(keypress);
-
-                    fieldText = field.getText();
-                    isEditing = false;
-                    field.setEnabled(false);
-                    return;
-                }
-                // Pass down to field.
-                super.onKeypress(keypress);
-            }
-
-            if (keypress.equals(kbEnter) || keypress.equals(kbF2)) {
-                // Enter or F2 starts editing.
-                fieldText = field.getText();
-                isEditing = true;
-                field.setEnabled(true);
-                activate(field);
-                return;
-            }
-        }
-
-        // --------------------------------------------------------------------
-        // TWidget ------------------------------------------------------------
-        // --------------------------------------------------------------------
-
-        /**
-         * Draw this cell.
-         */
-        @Override
-        public void draw() {
-            TTableWidget table = (TTableWidget) getParent();
-
-            if (isAbsoluteActive()) {
-                if (isEditing) {
-                    field.setActiveColorKey("tfield.active");
-                    field.setInactiveColorKey("tfield.inactive");
-                } else {
-                    field.setActiveColorKey("ttable.selected");
-                    field.setInactiveColorKey("ttable.selected");
-                }
-            } else if (((table.selectedColumn == column)
-                    && ((table.selectedRow == row)
-                        || (table.highlightColumn == true)))
-                || ((table.selectedRow == row)
-                    && ((table.selectedColumn == column)
-                        || (table.highlightRow == true)))
-            ) {
-                field.setActiveColorKey("ttable.active");
-                field.setInactiveColorKey("ttable.active");
-            } else {
-                field.setActiveColorKey("ttable.active");
-                field.setInactiveColorKey("ttable.inactive");
-            }
-
-            assert (isVisible() == true);
-
-            super.draw();
-        }
-
-        // --------------------------------------------------------------------
-        // TTable.Cell --------------------------------------------------------
-        // --------------------------------------------------------------------
-
-        /**
-         * Get field text.
-         *
-         * @return field text
-         */
-        public final String getText() {
-            return field.getText();
-        }
-
-        /**
-         * Set field text.
-         *
-         * @param text the new field text
-         */
-        public void setText(final String text) {
-            field.setText(text);
-        }
-
-        /**
-         * Cancel any pending edit.
-         */
-        public void cancelEdit() {
-            // Cancel any pending edit.
-            if (fieldText != null) {
-                field.setText(fieldText);
-            }
-            isEditing = false;
-            field.setEnabled(false);
-        }
-
-        /**
-         * Set an entire column of cells read-only (non-editable) or not.
-         *
-         * @param readOnly if true, the cells will be non-editable
-         */
-        public void setReadOnly(final boolean readOnly) {
-            cancelEdit();
-            this.readOnly = readOnly;
-        }
-
-    }
-
-    // ------------------------------------------------------------------------
-    // Constructors -----------------------------------------------------------
-    // ------------------------------------------------------------------------
-
-    /**
-     * Public 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
-     * @param gridColumns number of columns in grid
-     * @param gridRows number of rows in grid
-     */
-    public TTableWidget(final TWidget parent, final int x, final int y,
-        final int width, final int height, final int gridColumns,
-        final int gridRows) {
-
-        super(parent, x, y, width, height);
-
-        /*
-        System.err.println("gridColumns " + gridColumns +
-            " gridRows " + gridRows);
-         */
-
-        if (gridColumns < 1) {
-            throw new IllegalArgumentException("Column count cannot be less " +
-                "than 1");
-        }
-        if (gridRows < 1) {
-            throw new IllegalArgumentException("Row count cannot be less " +
-                "than 1");
-        }
-
-        // Initialize the starting row and column.
-        rows.add(new Row(0));
-        columns.add(new Column(0));
-        assert (rows.get(0).height == 1);
-
-        // Place a grid of cells that fit in this space.
-        for (int row = 0; row < gridRows; row++) {
-            for (int column = 0; column < gridColumns; column++) {
-                Cell cell = new Cell(this, 0, 0, COLUMN_DEFAULT_WIDTH, 1,
-                    column, row);
-
-                if (DEBUG) {
-                    // For debugging: set a grid of cell index labels.
-                    cell.setText("" + row + " " + column);
-                }
-                rows.get(row).add(cell);
-                columns.get(column).add(cell);
-
-                if (columns.size() < gridColumns) {
-                    columns.add(new Column(column + 1));
-                }
-            }
-            if (row < gridRows - 1) {
-                rows.add(new Row(row + 1));
-            }
-        }
-        for (int i = 0; i < rows.size(); i++) {
-            rows.get(i).setY(i + (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0));
-        }
-        for (int j = 0; j < columns.size(); j++) {
-            columns.get(j).setX((j * (COLUMN_DEFAULT_WIDTH + 1)) +
-                (showRowLabels ? ROW_LABEL_WIDTH : 0));
-        }
-        activate(columns.get(selectedColumn).get(selectedRow));
-
-        alignGrid();
-    }
-
-    /**
-     * Public 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
-     */
-    public TTableWidget(final TWidget parent, final int x, final int y,
-        final int width, final int height) {
-
-        this(parent, x, y, width, height,
-            width / (COLUMN_DEFAULT_WIDTH + 1) + EXTRA_COLUMNS,
-            height + EXTRA_ROWS);
-    }
-
-    // ------------------------------------------------------------------------
-    // Event handlers ---------------------------------------------------------
-    // ------------------------------------------------------------------------
-
-    /**
-     * Handle mouse press events.
-     *
-     * @param mouse mouse button press event
-     */
-    @Override
-    public void onMouseDown(final TMouseEvent mouse) {
-        if (mouse.isMouseWheelUp() || mouse.isMouseWheelDown()) {
-            // Treat wheel up/down as 3 up/down
-            TKeypressEvent keyEvent;
-            if (mouse.isMouseWheelUp()) {
-                keyEvent = new TKeypressEvent(kbUp);
-            } else {
-                keyEvent = new TKeypressEvent(kbDown);
-            }
-            for (int i = 0; i < 3; i++) {
-                onKeypress(keyEvent);
-            }
-            return;
-        }
-
-        // Use TWidget's code to pass the event to the children.
-        super.onMouseDown(mouse);
-    }
-
-    /**
-     * Handle keystrokes.
-     *
-     * @param keypress keystroke event
-     */
-    @Override
-    public void onKeypress(final TKeypressEvent keypress) {
-        if (keypress.equals(kbTab)
-            || keypress.equals(kbShiftTab)
-        ) {
-            // Squash tab and back-tab.  They don't make sense in the TTable
-            // grid context.
-            return;
-        }
-
-        // If editing, pass to that cell and do nothing else.
-        if (getSelectedCell().isEditing) {
-            super.onKeypress(keypress);
-            return;
-        }
-
-        if (keypress.equals(kbLeft)) {
-            // Left
-            if (selectedColumn > 0) {
-                selectedColumn--;
-            }
-            activate(columns.get(selectedColumn).get(selectedRow));
-        } else if (keypress.equals(kbRight)) {
-            // Right
-            if (selectedColumn < columns.size() - 1) {
-                selectedColumn++;
-            }
-            activate(columns.get(selectedColumn).get(selectedRow));
-        } else if (keypress.equals(kbUp)) {
-            // Up
-            if (selectedRow > 0) {
-                selectedRow--;
-            }
-            activate(columns.get(selectedColumn).get(selectedRow));
-        } else if (keypress.equals(kbDown)) {
-            // Down
-            if (selectedRow < rows.size() - 1) {
-                selectedRow++;
-            }
-            activate(columns.get(selectedColumn).get(selectedRow));
-        } else if (keypress.equals(kbHome)) {
-            // Home - leftmost column
-            selectedColumn = 0;
-            activate(columns.get(selectedColumn).get(selectedRow));
-        } else if (keypress.equals(kbEnd)) {
-            // End - rightmost column
-            selectedColumn = columns.size() - 1;
-            activate(columns.get(selectedColumn).get(selectedRow));
-        } else if (keypress.equals(kbPgUp)) {
-            // PgUp - Treat like multiple up
-            for (int i = 0; i < getHeight() - 2; i++) {
-                if (selectedRow > 0) {
-                    selectedRow--;
-                }
-            }
-            activate(columns.get(selectedColumn).get(selectedRow));
-        } else if (keypress.equals(kbPgDn)) {
-            // PgDn - Treat like multiple up
-            for (int i = 0; i < getHeight() - 2; i++) {
-                if (selectedRow < rows.size() - 1) {
-                    selectedRow++;
-                }
-            }
-            activate(columns.get(selectedColumn).get(selectedRow));
-        } else if (keypress.equals(kbCtrlHome)) {
-            // Ctrl-Home - go to top-left
-            selectedRow = 0;
-            selectedColumn = 0;
-            activate(columns.get(selectedColumn).get(selectedRow));
-            activate(columns.get(selectedColumn).get(selectedRow));
-        } else if (keypress.equals(kbCtrlEnd)) {
-            // Ctrl-End - go to bottom-right
-            selectedRow = rows.size() - 1;
-            selectedColumn = columns.size() - 1;
-            activate(columns.get(selectedColumn).get(selectedRow));
-            activate(columns.get(selectedColumn).get(selectedRow));
-        } else {
-            // Pass to the Cell.
-            super.onKeypress(keypress);
-        }
-
-        // We may have scrolled off screen.  Reset positions as needed to
-        // make the newly selected cell visible.
-        alignGrid();
-    }
-
-    /**
-     * Handle widget resize events.
-     *
-     * @param event resize event
-     */
-    @Override
-    public void onResize(final TResizeEvent event) {
-        super.onResize(event);
-
-        bottomRightCorner();
-    }
-
-    // ------------------------------------------------------------------------
-    // TWidget ----------------------------------------------------------------
-    // ------------------------------------------------------------------------
-
-    /**
-     * Draw the table row/column labels, and borders.
-     */
-    @Override
-    public void draw() {
-        CellAttributes labelColor = getTheme().getColor("ttable.label");
-        CellAttributes labelColorSelected = getTheme().getColor("ttable.label.selected");
-        CellAttributes borderColor = getTheme().getColor("ttable.border");
-
-        // Column labels.
-        if (showColumnLabels == true) {
-            for (int i = left; i < columns.size(); i++) {
-                if (columns.get(i).get(top).isVisible() == false) {
-                    break;
-                }
-                putStringXY(columns.get(i).get(top).getX(), 0,
-                    String.format(" %-" +
-                        (columns.get(i).width - 2)
-                        + "s ", columns.get(i).label),
-                    (i == selectedColumn ? labelColorSelected : labelColor));
-            }
-        }
-
-        // Row labels.
-        if (showRowLabels == true) {
-            for (int i = top; i < rows.size(); i++) {
-                if (rows.get(i).get(left).isVisible() == false) {
-                    break;
-                }
-                putStringXY(0, rows.get(i).get(left).getY(),
-                    String.format(" %-6s ", rows.get(i).label),
-                    (i == selectedRow ? labelColorSelected : labelColor));
-            }
-        }
-
-        // Draw vertical borders.
-        if (leftBorder == Border.SINGLE) {
-            vLineXY((showRowLabels ? ROW_LABEL_WIDTH : 0),
-                (topBorder == Border.NONE ? 0 : 1) +
-                    (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0),
-                getHeight(), '\u2502', borderColor);
-        }
-        for (int i = left; i < columns.size(); i++) {
-            if (columns.get(i).get(top).isVisible() == false) {
-                break;
-            }
-            if (columns.get(i).rightBorder == Border.SINGLE) {
-                vLineXY(columns.get(i).getX() + columns.get(i).width,
-                    (topBorder == Border.NONE ? 0 : 1) +
-                        (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0),
-                    getHeight(), '\u2502', borderColor);
-            }
-        }
-
-        // Draw horizontal borders.
-        if (topBorder == Border.SINGLE) {
-            hLineXY((showRowLabels ? ROW_LABEL_WIDTH : 0),
-                (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0),
-                getWidth(), '\u2500', borderColor);
-        }
-        for (int i = top; i < rows.size(); i++) {
-            if (rows.get(i).get(left).isVisible() == false) {
-                break;
-            }
-            if (rows.get(i).bottomBorder == Border.SINGLE) {
-                hLineXY((leftBorder == Border.NONE ? 0 : 1) +
-                        (showRowLabels ? ROW_LABEL_WIDTH : 0),
-                    rows.get(i).getY() + rows.get(i).height - 1,
-                    getWidth(), '\u2500', borderColor);
-            } else if (rows.get(i).bottomBorder == Border.DOUBLE) {
-                hLineXY((leftBorder == Border.NONE ? 0 : 1) +
-                        (showRowLabels ? ROW_LABEL_WIDTH : 0),
-                    rows.get(i).getY() + rows.get(i).height - 1,
-                    getWidth(), '\u2550', borderColor);
-            } else if (rows.get(i).bottomBorder == Border.THICK) {
-                hLineXY((leftBorder == Border.NONE ? 0 : 1) +
-                        (showRowLabels ? ROW_LABEL_WIDTH : 0),
-                    rows.get(i).getY() + rows.get(i).height - 1,
-                    getWidth(), '\u2501', borderColor);
-            }
-        }
-        // Top-left corner if needed
-        if ((topBorder == Border.SINGLE) && (leftBorder == Border.SINGLE)) {
-            putCharXY((showRowLabels ? ROW_LABEL_WIDTH : 0),
-                (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0),
-                '\u250c', borderColor);
-        }
-
-        // Now draw the correct corners
-        for (int i = top; i < rows.size(); i++) {
-            if (rows.get(i).get(left).isVisible() == false) {
-                break;
-            }
-            for (int j = left; j < columns.size(); j++) {
-                if (columns.get(j).get(i).isVisible() == false) {
-                    break;
-                }
-                if ((i == top) && (topBorder == Border.SINGLE)
-                    && (columns.get(j).rightBorder == Border.SINGLE)
-                ) {
-                    // Top tee
-                    putCharXY(columns.get(j).getX() + columns.get(j).width,
-                        (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0),
-                        '\u252c', borderColor);
-                }
-                if ((j == left) && (leftBorder == Border.SINGLE)
-                    && (rows.get(i).bottomBorder == Border.SINGLE)
-                ) {
-                    // Left tee
-                    putCharXY((showRowLabels ? ROW_LABEL_WIDTH : 0),
-                        rows.get(i).getY() + rows.get(i).height - 1,
-                        '\u251c', borderColor);
-                }
-                if ((columns.get(j).rightBorder == Border.SINGLE)
-                    && (rows.get(i).bottomBorder == Border.SINGLE)
-                ) {
-                    // Intersection of single bars
-                    putCharXY(columns.get(j).getX() + columns.get(j).width,
-                        rows.get(i).getY() + rows.get(i).height - 1,
-                        '\u253c', borderColor);
-                }
-                if ((j == left) && (leftBorder == Border.SINGLE)
-                    && (rows.get(i).bottomBorder == Border.DOUBLE)
-                ) {
-                    // Left tee: single bar vertical, double bar horizontal
-                    putCharXY((showRowLabels ? ROW_LABEL_WIDTH : 0),
-                        rows.get(i).getY() + rows.get(i).height - 1,
-                        '\u255e', borderColor);
-                }
-                if ((j == left) && (leftBorder == Border.SINGLE)
-                    && (rows.get(i).bottomBorder == Border.THICK)
-                ) {
-                    // Left tee: single bar vertical, thick bar horizontal
-                    putCharXY((showRowLabels ? ROW_LABEL_WIDTH : 0),
-                        rows.get(i).getY() + rows.get(i).height - 1,
-                        '\u251d', borderColor);
-                }
-                if ((columns.get(j).rightBorder == Border.SINGLE)
-                    && (rows.get(i).bottomBorder == Border.DOUBLE)
-                ) {
-                    // Intersection: single bar vertical, double bar
-                    // horizontal
-                    putCharXY(columns.get(j).getX() + columns.get(j).width,
-                        rows.get(i).getY() + rows.get(i).height - 1,
-                        '\u256a', borderColor);
-                }
-                if ((columns.get(j).rightBorder == Border.SINGLE)
-                    && (rows.get(i).bottomBorder == Border.THICK)
-                ) {
-                    // Intersection: single bar vertical, thick bar
-                    // horizontal
-                    putCharXY(columns.get(j).getX() + columns.get(j).width,
-                        rows.get(i).getY() + rows.get(i).height - 1,
-                        '\u253f', borderColor);
-                }
-            }
-        }
-
-        // Now draw the window borders.
-        super.draw();
-    }
-
-    // ------------------------------------------------------------------------
-    // TTable -----------------------------------------------------------------
-    // ------------------------------------------------------------------------
-
-    /**
-     * Generate the default letter name for a column number.
-     *
-     * @param col column number to use for this column.  Column 0 will be
-     * "A", column 1 will be "B", column 26 will be "AA", and so on.
-     */
-    private String makeColumnLabel(int col) {
-        StringBuilder sb = new StringBuilder();
-        for (;;) {
-            sb.append((char) ('A' + (col % 26)));
-            if (col < 26) {
-                break;
-            }
-            col /= 26;
-        }
-        return sb.reverse().toString();
-    }
-
-    /**
-     * Get the currently-selected cell.
-     *
-     * @return the selected cell
-     */
-    public Cell getSelectedCell() {
-        assert (rows.get(selectedRow) != null);
-        assert (rows.get(selectedRow).get(selectedColumn) != null);
-        assert (columns.get(selectedColumn) != null);
-        assert (columns.get(selectedColumn).get(selectedRow) != null);
-        assert (rows.get(selectedRow).get(selectedColumn) ==
-            columns.get(selectedColumn).get(selectedRow));
-
-        return (columns.get(selectedColumn).get(selectedRow));
-    }
-
-    /**
-     * Get the currently-selected column.
-     *
-     * @return the selected column
-     */
-    public Column getSelectedColumn() {
-        assert (selectedColumn >= 0);
-        assert (columns.size() > selectedColumn);
-        assert (columns.get(selectedColumn) != null);
-        return columns.get(selectedColumn);
-    }
-
-    /**
-     * Get the currently-selected row.
-     *
-     * @return the selected row
-     */
-    public Row getSelectedRow() {
-        assert (selectedRow >= 0);
-        assert (rows.size() > selectedRow);
-        assert (rows.get(selectedRow) != null);
-        return rows.get(selectedRow);
-    }
-
-    /**
-     * Get the currently-selected column number.  0 is the left-most column.
-     *
-     * @return the selected column number
-     */
-    public int getSelectedColumnNumber() {
-        return selectedColumn;
-    }
-
-    /**
-     * Set the currently-selected column number.  0 is the left-most column.
-     *
-     * @param column the column number to select
-     */
-    public void setSelectedColumnNumber(final int column) {
-        if ((column < 0) || (column > columns.size() - 1)) {
-            throw new IndexOutOfBoundsException("Column count is " +
-                columns.size() + ", requested index " + column);
-        }
-        selectedColumn = column;
-        activate(columns.get(selectedColumn).get(selectedRow));
-        alignGrid();
-    }
-
-    /**
-     * Get the currently-selected row number.  0 is the top-most row.
-     *
-     * @return the selected row number
-     */
-    public int getSelectedRowNumber() {
-        return selectedRow;
-    }
-
-    /**
-     * Set the currently-selected row number.  0 is the left-most column.
-     *
-     * @param row the row number to select
-     */
-    public void setSelectedRowNumber(final int row) {
-        if ((row < 0) || (row > rows.size() - 1)) {
-            throw new IndexOutOfBoundsException("Row count is " +
-                rows.size() + ", requested index " + row);
-        }
-        selectedRow = row;
-        activate(columns.get(selectedColumn).get(selectedRow));
-        alignGrid();
-    }
-
-    /**
-     * Get the highlight row flag.
-     *
-     * @return true if the selected row is highlighted
-     */
-    public boolean getHighlightRow() {
-        return highlightRow;
-    }
-
-    /**
-     * Set the highlight row flag.
-     *
-     * @param highlightRow if true, the selected row will be highlighted
-     */
-    public void setHighlightRow(final boolean highlightRow) {
-        this.highlightRow = highlightRow;
-    }
-
-    /**
-     * Get the highlight column flag.
-     *
-     * @return true if the selected column is highlighted
-     */
-    public boolean getHighlightColumn() {
-        return highlightColumn;
-    }
-
-    /**
-     * Set the highlight column flag.
-     *
-     * @param highlightColumn if true, the selected column will be highlighted
-     */
-    public void setHighlightColumn(final boolean highlightColumn) {
-        this.highlightColumn = highlightColumn;
-    }
-
-    /**
-     * Get the show row labels flag.
-     *
-     * @return true if row labels are shown
-     */
-    public boolean getShowRowLabels() {
-        return showRowLabels;
-    }
-
-    /**
-     * Set the show row labels flag.
-     *
-     * @param showRowLabels if true, the row labels will be shown
-     */
-    public void setShowRowLabels(final boolean showRowLabels) {
-        this.showRowLabels = showRowLabels;
-    }
-
-    /**
-     * Get the show column labels flag.
-     *
-     * @return true if column labels are shown
-     */
-    public boolean getShowColumnLabels() {
-        return showColumnLabels;
-    }
-
-    /**
-     * Set the show column labels flag.
-     *
-     * @param showColumnLabels if true, the column labels will be shown
-     */
-    public void setShowColumnLabels(final boolean showColumnLabels) {
-        this.showColumnLabels = showColumnLabels;
-    }
-
-    /**
-     * Get the number of columns.
-     *
-     * @return the number of columns
-     */
-    public int getColumnCount() {
-        return columns.size();
-    }
-
-    /**
-     * Get the number of rows.
-     *
-     * @return the number of rows
-     */
-    public int getRowCount() {
-        return rows.size();
-    }
-
-
-    /**
-     * Push top and left to the bottom-most right corner of the available
-     * grid.
-     */
-    private void bottomRightCorner() {
-        int viewColumns = getWidth();
-        if (showRowLabels == true) {
-            viewColumns -= ROW_LABEL_WIDTH;
-        }
-
-        // Set left and top such that the table stays on screen if possible.
-        top = rows.size() - getHeight();
-        left = columns.size() - (getWidth() / (viewColumns / (COLUMN_DEFAULT_WIDTH + 1)));
-        // Now ensure the selection is visible.
-        alignGrid();
-    }
-
-    /**
-     * Align the grid so that the selected cell is fully visible.
-     */
-    private void alignGrid() {
-
-        /*
-        System.err.println("alignGrid() # columns " + columns.size() +
-            " # rows " + rows.size());
-         */
-
-        int viewColumns = getWidth();
-        if (showRowLabels == true) {
-            viewColumns -= ROW_LABEL_WIDTH;
-        }
-        if (leftBorder != Border.NONE) {
-            viewColumns--;
-        }
-        int viewRows = getHeight();
-        if (showColumnLabels == true) {
-            viewRows -= COLUMN_LABEL_HEIGHT;
-        }
-        if (topBorder != Border.NONE) {
-            viewRows--;
-        }
-
-        // If we pushed left or right, adjust the box to include the new
-        // selected cell.
-        if (selectedColumn < left) {
-            left = selectedColumn - 1;
-        }
-        if (left < 0) {
-            left = 0;
-        }
-        if (selectedRow < top) {
-            top = selectedRow - 1;
-        }
-        if (top < 0) {
-            top = 0;
-        }
-
-        /*
-         * viewColumns and viewRows now contain the available columns and
-         * rows available to view the selected cell.  We adjust left and top
-         * to ensure the selected cell is within view, and then make all
-         * cells outside the box between (left, top) and (right, bottom)
-         * invisible.
-         *
-         * We need to calculate right and bottom now.
-         */
-        int right = left;
-
-        boolean done = false;
-        while (!done) {
-            int rightCellX = (showRowLabels ? ROW_LABEL_WIDTH : 0);
-            if (leftBorder != Border.NONE) {
-                rightCellX++;
-            }
-            int maxCellX = rightCellX + viewColumns;
-            right = left;
-            boolean selectedIsVisible = false;
-            int selectedX = 0;
-            for (int x = left; x < columns.size(); x++) {
-                if (x == selectedColumn) {
-                    selectedX = rightCellX;
-                    if (selectedX + columns.get(x).width + 1 <= maxCellX) {
-                        selectedIsVisible = true;
-                    }
-                }
-                rightCellX += columns.get(x).width + 1;
-                if (rightCellX >= maxCellX) {
-                    break;
-                }
-                right++;
-            }
-            if (right < selectedColumn) {
-                // selectedColumn is outside the view range.  Push left over,
-                // and calculate again.
-                left++;
-            } else if (left == selectedColumn) {
-                // selectedColumn doesn't fit inside the view range, but we
-                // can't go over any further either.  Bail out.
-                done = true;
-            } else if (selectedIsVisible == false) {
-                // selectedColumn doesn't fit inside the view range, continue
-                // on.
-                left++;
-            } else {
-                // selectedColumn is fully visible, all done.
-                assert (selectedIsVisible == true);
-                done = true;
-            }
-
-        } // while (!done)
-
-        // We have the left/right range correct, set cell visibility and
-        // column X positions.
-        int leftCellX = showRowLabels ? ROW_LABEL_WIDTH : 0;
-        if (leftBorder != Border.NONE) {
-            leftCellX++;
-        }
-        for (int x = 0; x < columns.size(); x++) {
-            if ((x < left) || (x > right)) {
-                for (int i = 0; i < rows.size(); i++) {
-                    columns.get(x).get(i).setVisible(false);
-                    columns.get(x).setX(getWidth() + 1);
-                }
-                continue;
-            }
-            for (int i = 0; i < rows.size(); i++) {
-                columns.get(x).get(i).setVisible(true);
-            }
-            columns.get(x).setX(leftCellX);
-            leftCellX += columns.get(x).width + 1;
-        }
-
-        int bottom = top;
-
-        done = false;
-        while (!done) {
-            int bottomCellY = (showColumnLabels ? COLUMN_LABEL_HEIGHT : 0);
-            if (topBorder != Border.NONE) {
-                bottomCellY++;
-            }
-            int maxCellY = bottomCellY + viewRows;
-            bottom = top;
-            for (int y = top; y < rows.size(); y++) {
-                bottomCellY += rows.get(y).height;
-                if (bottomCellY >= maxCellY) {
-                    break;
-                }
-                bottom++;
-            }
-            if (bottom < selectedRow) {
-                // selectedRow is outside the view range.  Push top down, and
-                // calculate again.
-                top++;
-            } else {
-                // selectedRow is inside the view range, done.
-                done = true;
-            }
-        } // while (!done)
-
-        // We have the top/bottom range correct, set cell visibility and
-        // row Y positions.
-        int topCellY = showColumnLabels ? COLUMN_LABEL_HEIGHT : 0;
-        if (topBorder != Border.NONE) {
-            topCellY++;
-        }
-        for (int y = 0; y < rows.size(); y++) {
-            if ((y < top) || (y > bottom)) {
-                for (int i = 0; i < columns.size(); i++) {
-                    rows.get(y).get(i).setVisible(false);
-                }
-                rows.get(y).setY(getHeight() + 1);
-                continue;
-            }
-            for (int i = 0; i < columns.size(); i++) {
-                rows.get(y).get(i).setVisible(true);
-            }
-            rows.get(y).setY(topCellY);
-            topCellY += rows.get(y).height;
-        }
-
-        // Last thing: cancel any edits that are not the selected cell.
-        for (int y = 0; y < rows.size(); y++) {
-            for (int x = 0; x < columns.size(); x++) {
-                if ((x == selectedColumn) && (y == selectedRow)) {
-                    continue;
-                }
-                rows.get(y).get(x).cancelEdit();
-            }
-        }
-    }
-
-    /**
-     * Load contents from file in CSV format.
-     *
-     * @param csvFile a File referencing the CSV data
-     * @throws IOException if a java.io operation throws
-     */
-    public void loadCsvFile(final File csvFile) throws IOException {
-        BufferedReader reader = null;
-
-        try {
-            reader = new BufferedReader(new FileReader(csvFile));
-
-            String line = null;
-            boolean first = true;
-            for (line = reader.readLine(); line != null;
-                 line = reader.readLine()) {
-
-                List<String> list = StringUtils.fromCsv(line);
-                if (list.size() == 0) {
-                    continue;
-                }
-
-                if (list.size() > columns.size()) {
-                    int n = list.size() - columns.size();
-                    for (int i = 0; i < n; i++) {
-                        selectedColumn = columns.size() - 1;
-                        insertColumnRight(selectedColumn);
-                    }
-                }
-                assert (list.size() == columns.size());
-
-                if (first) {
-                    // First row: just replace what is here.
-                    selectedRow = 0;
-                    first = false;
-                } else {
-                    // All other rows: append to the end.
-                    selectedRow = rows.size() - 1;
-                    insertRowBelow(selectedRow);
-                    selectedRow = rows.size() - 1;
-                }
-                for (int i = 0; i < list.size(); i++) {
-                    rows.get(selectedRow).get(i).setText(list.get(i));
-                }
-
-                // TODO: detect header line
-            }
-        } finally {
-            if (reader != null) {
-                reader.close();
-            }
-        }
-
-        left = 0;
-        top = 0;
-        selectedRow = 0;
-        selectedColumn = 0;
-        alignGrid();
-        activate(columns.get(selectedColumn).get(selectedRow));
-    }
-
-    /**
-     * Save contents to file in CSV format.
-     *
-     * @param filename file to save to
-     * @throws IOException if a java.io operation throws
-     */
-    public void saveToCsvFilename(final String filename) throws IOException {
-        BufferedWriter writer = null;
-
-        try {
-            writer = new BufferedWriter(new FileWriter(filename));
-            for (Row row: rows) {
-                List<String> list = new ArrayList<String>(row.cells.size());
-                for (Cell cell: row.cells) {
-                    list.add(cell.getText());
-                }
-                writer.write(StringUtils.toCsv(list));
-                writer.write("\n");
-            }
-        } finally {
-            if (writer != null) {
-                writer.close();
-            }
-        }
-    }
-
-    /**
-     * Save contents to file in text format with lines.
-     *
-     * @param filename file to save to
-     * @throws IOException if a java.io operation throws
-     */
-    public void saveToTextFilename(final String filename) throws IOException {
-        BufferedWriter writer = null;
-
-        try {
-            writer = new BufferedWriter(new FileWriter(filename));
-
-            if ((topBorder == Border.SINGLE) && (leftBorder == Border.SINGLE)) {
-                // Emit top-left corner.
-                writer.write("\u250c");
-            }
-
-            if (topBorder == Border.SINGLE) {
-                int cellI = 0;
-                for (Cell cell: rows.get(0).cells) {
-                    for (int i = 0; i < columns.get(cellI).width; i++) {
-                        writer.write("\u2500");
-                    }
-
-                    if (columns.get(cellI).rightBorder == Border.SINGLE) {
-                        if (cellI < columns.size() - 1) {
-                            // Emit top tee.
-                            writer.write("\u252c");
-                        } else {
-                            // Emit top-right corner.
-                            writer.write("\u2510");
-                        }
-                    }
-                    cellI++;
-                }
-            }
-            writer.write("\n");
-
-            int rowI = 0;
-            for (Row row: rows) {
-
-                if (leftBorder == Border.SINGLE) {
-                    // Emit left border.
-                    writer.write("\u2502");
-                }
-
-                int cellI = 0;
-                for (Cell cell: row.cells) {
-                    writer.write(String.format("%" +
-                            columns.get(cellI).width + "s", cell.getText()));
-
-                    if (columns.get(cellI).rightBorder == Border.SINGLE) {
-                        // Emit right border.
-                        writer.write("\u2502");
-                    }
-                    cellI++;
-                }
-                writer.write("\n");
-
-                if (row.bottomBorder == Border.NONE) {
-                    // All done, move on to the next row.
-                    continue;
-                }
-
-                // Emit the bottom borders and intersections.
-                if ((leftBorder == Border.SINGLE)
-                    && (row.bottomBorder != Border.NONE)
-                ) {
-                    if (rowI < rows.size() - 1) {
-                        if (row.bottomBorder == Border.SINGLE) {
-                            // Emit left tee.
-                            writer.write("\u251c");
-                        } else if (row.bottomBorder == Border.DOUBLE) {
-                            // Emit left tee (double).
-                            writer.write("\u255e");
-                        } else if (row.bottomBorder == Border.THICK) {
-                            // Emit left tee (thick).
-                            writer.write("\u251d");
-                        }
-                    }
-
-                    if (rowI == rows.size() - 1) {
-                        if (row.bottomBorder == Border.SINGLE) {
-                            // Emit left bottom corner.
-                            writer.write("\u2514");
-                        } else if (row.bottomBorder == Border.DOUBLE) {
-                            // Emit left bottom corner (double).
-                            writer.write("\u2558");
-                        } else if (row.bottomBorder == Border.THICK) {
-                            // Emit left bottom corner (thick).
-                            writer.write("\u2515");
-                        }
-                    }
-                }
-
-                cellI = 0;
-                for (Cell cell: row.cells) {
-
-                    for (int i = 0; i < columns.get(cellI).width; i++) {
-                        if (row.bottomBorder == Border.SINGLE) {
-                            writer.write("\u2500");
-                        }
-                        if (row.bottomBorder == Border.DOUBLE) {
-                            writer.write("\u2550");
-                        }
-                        if (row.bottomBorder == Border.THICK) {
-                            writer.write("\u2501");
-                        }
-                    }
-
-                    if ((rowI < rows.size() - 1)
-                        && (cellI == columns.size() - 1)
-                        && (row.bottomBorder == Border.SINGLE)
-                        && (columns.get(cellI).rightBorder == Border.SINGLE)
-                    ) {
-                        // Emit right tee.
-                        writer.write("\u2524");
-                    }
-                    if ((rowI < rows.size() - 1)
-                        && (cellI == columns.size() - 1)
-                        && (row.bottomBorder == Border.DOUBLE)
-                        && (columns.get(cellI).rightBorder == Border.SINGLE)
-                    ) {
-                        // Emit right tee (double).
-                        writer.write("\u2561");
-                    }
-                    if ((rowI < rows.size() - 1)
-                        && (cellI == columns.size() - 1)
-                        && (row.bottomBorder == Border.THICK)
-                        && (columns.get(cellI).rightBorder == Border.SINGLE)
-                    ) {
-                        // Emit right tee (thick).
-                        writer.write("\u2525");
-                    }
-                    if ((rowI == rows.size() - 1)
-                        && (cellI == columns.size() - 1)
-                        && (row.bottomBorder == Border.SINGLE)
-                        && (columns.get(cellI).rightBorder == Border.SINGLE)
-                    ) {
-                        // Emit right bottom corner.
-                        writer.write("\u2518");
-                    }
-                    if ((rowI == rows.size() - 1)
-                        && (cellI == columns.size() - 1)
-                        && (row.bottomBorder == Border.DOUBLE)
-                        && (columns.get(cellI).rightBorder == Border.SINGLE)
-                    ) {
-                        // Emit right bottom corner (double).
-                        writer.write("\u255b");
-                    }
-                    if ((rowI == rows.size() - 1)
-                        && (cellI == columns.size() - 1)
-                        && (row.bottomBorder == Border.THICK)
-                        && (columns.get(cellI).rightBorder == Border.SINGLE)
-                    ) {
-                        // Emit right bottom corner (thick).
-                        writer.write("\u2519");
-                    }
-                    if ((rowI < rows.size() - 1)
-                        && (cellI < columns.size() - 1)
-                        && (row.bottomBorder == Border.SINGLE)
-                        && (columns.get(cellI).rightBorder == Border.SINGLE)
-                    ) {
-                        // Emit intersection.
-                        writer.write("\u253c");
-                    }
-                    if ((rowI < rows.size() - 1)
-                        && (cellI < columns.size() - 1)
-                        && (row.bottomBorder == Border.DOUBLE)
-                        && (columns.get(cellI).rightBorder == Border.SINGLE)
-                    ) {
-                        // Emit intersection (double).
-                        writer.write("\u256a");
-                    }
-                    if ((rowI < rows.size() - 1)
-                        && (cellI < columns.size() - 1)
-                        && (row.bottomBorder == Border.THICK)
-                        && (columns.get(cellI).rightBorder == Border.SINGLE)
-                    ) {
-                        // Emit intersection (thick).
-                        writer.write("\u253f");
-                    }
-                    if ((rowI == rows.size() - 1)
-                        && (cellI < columns.size() - 1)
-                        && (row.bottomBorder == Border.SINGLE)
-                        && (columns.get(cellI).rightBorder == Border.SINGLE)
-                    ) {
-                        // Emit bottom tee.
-                        writer.write("\u2534");
-                    }
-                    if ((rowI == rows.size() - 1)
-                        && (cellI < columns.size() - 1)
-                        && (row.bottomBorder == Border.DOUBLE)
-                        && (columns.get(cellI).rightBorder == Border.SINGLE)
-                    ) {
-                        // Emit bottom tee (double).
-                        writer.write("\u2567");
-                    }
-                    if ((rowI == rows.size() - 1)
-                        && (cellI < columns.size() - 1)
-                        && (row.bottomBorder == Border.THICK)
-                        && (columns.get(cellI).rightBorder == Border.SINGLE)
-                    ) {
-                        // Emit bottom tee (thick).
-                        writer.write("\u2537");
-                    }
-
-                    cellI++;
-                }
-
-                writer.write("\n");
-                rowI++;
-            }
-        } finally {
-            if (writer != null) {
-                writer.close();
-            }
-        }
-    }
-
-    /**
-     * Set the selected cell location.
-     *
-     * @param column the selected cell location column
-     * @param row the selected cell location row
-     */
-    public void setSelectedCell(final int column, final int row) {
-        if ((column < 0) || (column > columns.size() - 1)) {
-            throw new IndexOutOfBoundsException("Column count is " +
-                columns.size() + ", requested index " + column);
-        }
-        if ((row < 0) || (row > rows.size() - 1)) {
-            throw new IndexOutOfBoundsException("Row count is " +
-                rows.size() + ", requested index " + row);
-        }
-        selectedColumn = column;
-        selectedRow = row;
-        alignGrid();
-    }
-
-    /**
-     * Get a particular cell.
-     *
-     * @param column the cell column
-     * @param row the cell row
-     * @return the cell
-     */
-    public Cell getCell(final int column, final int row) {
-        if ((column < 0) || (column > columns.size() - 1)) {
-            throw new IndexOutOfBoundsException("Column count is " +
-                columns.size() + ", requested index " + column);
-        }
-        if ((row < 0) || (row > rows.size() - 1)) {
-            throw new IndexOutOfBoundsException("Row count is " +
-                rows.size() + ", requested index " + row);
-        }
-        return rows.get(row).get(column);
-    }
-
-    /**
-     * Get the text of a particular cell.
-     *
-     * @param column the cell column
-     * @param row the cell row
-     * @return the text in the cell
-     */
-    public String getCellText(final int column, final int row) {
-        if ((column < 0) || (column > columns.size() - 1)) {
-            throw new IndexOutOfBoundsException("Column count is " +
-                columns.size() + ", requested index " + column);
-        }
-        if ((row < 0) || (row > rows.size() - 1)) {
-            throw new IndexOutOfBoundsException("Row count is " +
-                rows.size() + ", requested index " + row);
-        }
-        return rows.get(row).get(column).getText();
-    }
-
-    /**
-     * Set the text of a particular cell.
-     *
-     * @param column the cell column
-     * @param row the cell row
-     * @param text the text to put into the cell
-     */
-    public void setCellText(final int column, final int row,
-        final String text) {
-
-        if ((column < 0) || (column > columns.size() - 1)) {
-            throw new IndexOutOfBoundsException("Column count is " +
-                columns.size() + ", requested index " + column);
-        }
-        if ((row < 0) || (row > rows.size() - 1)) {
-            throw new IndexOutOfBoundsException("Row count is " +
-                rows.size() + ", requested index " + row);
-        }
-        rows.get(row).get(column).setText(text);
-    }
-
-    /**
-     * Set the action to perform when the user presses enter on a particular
-     * cell.
-     *
-     * @param column the cell column
-     * @param row the cell row
-     * @param action the action to perform when the user presses enter on the
-     * cell
-     */
-    public void setCellEnterAction(final int column, final int row,
-        final TAction action) {
-
-        if ((column < 0) || (column > columns.size() - 1)) {
-            throw new IndexOutOfBoundsException("Column count is " +
-                columns.size() + ", requested index " + column);
-        }
-        if ((row < 0) || (row > rows.size() - 1)) {
-            throw new IndexOutOfBoundsException("Row count is " +
-                rows.size() + ", requested index " + row);
-        }
-        rows.get(row).get(column).field.setEnterAction(action);
-    }
-
-    /**
-     * Set the action to perform when the user updates a particular cell.
-     *
-     * @param column the cell column
-     * @param row the cell row
-     * @param action the action to perform when the user updates the cell
-     */
-    public void setCellUpdateAction(final int column, final int row,
-        final TAction action) {
-
-        if ((column < 0) || (column > columns.size() - 1)) {
-            throw new IndexOutOfBoundsException("Column count is " +
-                columns.size() + ", requested index " + column);
-        }
-        if ((row < 0) || (row > rows.size() - 1)) {
-            throw new IndexOutOfBoundsException("Row count is " +
-                rows.size() + ", requested index " + row);
-        }
-        rows.get(row).get(column).field.setUpdateAction(action);
-    }
-
-    /**
-     * Get the width of a column.
-     *
-     * @param column the column number
-     * @return the width of the column
-     */
-    public int getColumnWidth(final int column) {
-        if ((column < 0) || (column > columns.size() - 1)) {
-            throw new IndexOutOfBoundsException("Column count is " +
-                columns.size() + ", requested index " + column);
-        }
-        return columns.get(column).width;
-    }
-
-    /**
-     * Set the width of a column.
-     *
-     * @param column the column number
-     * @param width the new width of the column
-     */
-    public void setColumnWidth(final int column, final int width) {
-        if ((column < 0) || (column > columns.size() - 1)) {
-            throw new IndexOutOfBoundsException("Column count is " +
-                columns.size() + ", requested index " + column);
-        }
-
-        if (width < 4) {
-            // Columns may not be smaller than 4 cells wide.
-            return;
-        }
-
-        int delta = width - columns.get(column).width;
-        columns.get(column).width = width;
-        for (Cell cell: columns.get(column).cells) {
-            cell.setWidth(columns.get(column).width);
-            cell.field.setWidth(columns.get(column).width);
-        }
-        for (int i = column + 1; i < columns.size(); i++) {
-            columns.get(i).setX(columns.get(i).getX() + delta);
-        }
-        if (column == columns.size() - 1) {
-            bottomRightCorner();
-        } else {
-            alignGrid();
-        }
-    }
-
-    /**
-     * Get the label of a column.
-     *
-     * @param column the column number
-     * @return the label of the column
-     */
-    public String getColumnLabel(final int column) {
-        if ((column < 0) || (column > columns.size() - 1)) {
-            throw new IndexOutOfBoundsException("Column count is " +
-                columns.size() + ", requested index " + column);
-        }
-        return columns.get(column).label;
-    }
-
-    /**
-     * Set the label of a column.
-     *
-     * @param column the column number
-     * @param label the new label of the column
-     */
-    public void setColumnLabel(final int column, final String label) {
-        if ((column < 0) || (column > columns.size() - 1)) {
-            throw new IndexOutOfBoundsException("Column count is " +
-                columns.size() + ", requested index " + column);
-        }
-        columns.get(column).label = label;
-    }
-
-    /**
-     * Get the label of a row.
-     *
-     * @param row the row number
-     * @return the label of the row
-     */
-    public String getRowLabel(final int row) {
-        if ((row < 0) || (row > rows.size() - 1)) {
-            throw new IndexOutOfBoundsException("Row count is " +
-                rows.size() + ", requested index " + row);
-        }
-        return rows.get(row).label;
-    }
-
-    /**
-     * Set the label of a row.
-     *
-     * @param row the row number
-     * @param label the new label of the row
-     */
-    public void setRowLabel(final int row, final String label) {
-        if ((row < 0) || (row > rows.size() - 1)) {
-            throw new IndexOutOfBoundsException("Row count is " +
-                rows.size() + ", requested index " + row);
-        }
-        rows.get(row).label = label;
-    }
-
-    /**
-     * Insert one row at a particular index.
-     *
-     * @param idx the row number
-     */
-    private void insertRowAt(final int idx) {
-        Row newRow = new Row(idx);
-        for (int i = 0; i < columns.size(); i++) {
-            Cell cell = new Cell(this, columns.get(i).getX(),
-                rows.get(idx).getY(), COLUMN_DEFAULT_WIDTH, 1, i, idx);
-            newRow.add(cell);
-            columns.get(i).cells.add(idx, cell);
-        }
-        rows.add(idx, newRow);
-
-        for (int x = 0; x < columns.size(); x++) {
-            for (int y = idx; y < rows.size(); y++) {
-                columns.get(x).get(y).row = y;
-                columns.get(x).get(y).column = x;
-            }
-        }
-        for (int i = idx + 1; i < rows.size(); i++) {
-            String oldRowLabel = Integer.toString(i - 1);
-            if (rows.get(i).label.equals(oldRowLabel)) {
-                rows.get(i).label = Integer.toString(i);
-            }
-        }
-        alignGrid();
-    }
-
-    /**
-     * Insert one row above a particular row.
-     *
-     * @param row the row number
-     */
-    public void insertRowAbove(final int row) {
-        if ((row < 0) || (row > rows.size() - 1)) {
-            throw new IndexOutOfBoundsException("Row count is " +
-                rows.size() + ", requested index " + row);
-        }
-        insertRowAt(row);
-        selectedRow++;
-        activate(columns.get(selectedColumn).get(selectedRow));
-    }
-
-    /**
-     * Insert one row below a particular row.
-     *
-     * @param row the row number
-     */
-    public void insertRowBelow(final int row) {
-        if ((row < 0) || (row > rows.size() - 1)) {
-            throw new IndexOutOfBoundsException("Row count is " +
-                rows.size() + ", requested index " + row);
-        }
-        int idx = row + 1;
-        if (idx < rows.size()) {
-            insertRowAt(idx);
-            activate(columns.get(selectedColumn).get(selectedRow));
-            return;
-        }
-
-        // row is the last row, we need to perform an append.
-        Row newRow = new Row(idx);
-        for (int i = 0; i < columns.size(); i++) {
-            Cell cell = new Cell(this, columns.get(i).getX(),
-                rows.get(row).getY(), COLUMN_DEFAULT_WIDTH, 1, i, idx);
-            newRow.add(cell);
-            columns.get(i).cells.add(cell);
-        }
-        rows.add(newRow);
-        alignGrid();
-        activate(columns.get(selectedColumn).get(selectedRow));
-    }
-
-    /**
-     * Delete a particular row.
-     *
-     * @param row the row number
-     */
-    public void deleteRow(final int row) {
-        if ((row < 0) || (row > rows.size() - 1)) {
-            throw new IndexOutOfBoundsException("Row count is " +
-                rows.size() + ", requested index " + row);
-        }
-        if (rows.size() == 1) {
-            // Don't delete the last row.
-            return;
-        }
-        for (int i = 0; i < columns.size(); i++) {
-            Cell cell = columns.get(i).cells.remove(row);
-            getChildren().remove(cell);
-        }
-        rows.remove(row);
-
-        for (int x = 0; x < columns.size(); x++) {
-            for (int y = row; y < rows.size(); y++) {
-                columns.get(x).get(y).row = y;
-                columns.get(x).get(y).column = x;
-            }
-        }
-        for (int i = row; i < rows.size(); i++) {
-            String oldRowLabel = Integer.toString(i + 1);
-            if (rows.get(i).label.equals(oldRowLabel)) {
-                rows.get(i).label = Integer.toString(i);
-            }
-        }
-        if (selectedRow == rows.size()) {
-            selectedRow--;
-        }
-        activate(columns.get(selectedColumn).get(selectedRow));
-        bottomRightCorner();
-    }
-
-    /**
-     * Insert one column at a particular index.
-     *
-     * @param idx the column number
-     */
-    private void insertColumnAt(final int idx) {
-        Column newColumn = new Column(idx);
-        for (int i = 0; i < rows.size(); i++) {
-            Cell cell = new Cell(this, columns.get(idx).getX(),
-                rows.get(i).getY(), COLUMN_DEFAULT_WIDTH, 1, idx, i);
-            newColumn.add(cell);
-            rows.get(i).cells.add(idx, cell);
-        }
-        columns.add(idx, newColumn);
-
-        for (int x = idx; x < columns.size(); x++) {
-            for (int y = 0; y < rows.size(); y++) {
-                columns.get(x).get(y).row = y;
-                columns.get(x).get(y).column = x;
-            }
-        }
-        for (int i = idx + 1; i < columns.size(); i++) {
-            String oldColumnLabel = makeColumnLabel(i - 1);
-            if (columns.get(i).label.equals(oldColumnLabel)) {
-                columns.get(i).label = makeColumnLabel(i);
-            }
-        }
-        alignGrid();
-    }
-
-    /**
-     * Insert one column to the left of a particular column.
-     *
-     * @param column the column number
-     */
-    public void insertColumnLeft(final int column) {
-        if ((column < 0) || (column > columns.size() - 1)) {
-            throw new IndexOutOfBoundsException("Column count is " +
-                columns.size() + ", requested index " + column);
-        }
-        insertColumnAt(column);
-        selectedColumn++;
-        activate(columns.get(selectedColumn).get(selectedRow));
-    }
-
-    /**
-     * Insert one column to the right of a particular column.
-     *
-     * @param column the column number
-     */
-    public void insertColumnRight(final int column) {
-        if ((column < 0) || (column > columns.size() - 1)) {
-            throw new IndexOutOfBoundsException("Column count is " +
-                columns.size() + ", requested index " + column);
-        }
-        int idx = column + 1;
-        if (idx < columns.size()) {
-            insertColumnAt(idx);
-            activate(columns.get(selectedColumn).get(selectedRow));
-            return;
-        }
-
-        // column is the last column, we need to perform an append.
-        Column newColumn = new Column(idx);
-        for (int i = 0; i < rows.size(); i++) {
-            Cell cell = new Cell(this, columns.get(column).getX(),
-                rows.get(i).getY(), COLUMN_DEFAULT_WIDTH, 1, idx, i);
-            newColumn.add(cell);
-            rows.get(i).cells.add(cell);
-        }
-        columns.add(newColumn);
-        alignGrid();
-        activate(columns.get(selectedColumn).get(selectedRow));
-    }
-
-    /**
-     * Delete a particular column.
-     *
-     * @param column the column number
-     */
-    public void deleteColumn(final int column) {
-        if ((column < 0) || (column > columns.size() - 1)) {
-            throw new IndexOutOfBoundsException("Column count is " +
-                columns.size() + ", requested index " + column);
-        }
-        if (columns.size() == 1) {
-            // Don't delete the last column.
-            return;
-        }
-        for (int i = 0; i < rows.size(); i++) {
-            Cell cell = rows.get(i).cells.remove(column);
-            getChildren().remove(cell);
-        }
-        columns.remove(column);
-
-        for (int x = column; x < columns.size(); x++) {
-            for (int y = 0; y < rows.size(); y++) {
-                columns.get(x).get(y).row = y;
-                columns.get(x).get(y).column = x;
-            }
-        }
-        for (int i = column; i < columns.size(); i++) {
-            String oldColumnLabel = makeColumnLabel(i + 1);
-            if (columns.get(i).label.equals(oldColumnLabel)) {
-                columns.get(i).label = makeColumnLabel(i);
-            }
-        }
-        if (selectedColumn == columns.size()) {
-            selectedColumn--;
-        }
-        activate(columns.get(selectedColumn).get(selectedRow));
-        bottomRightCorner();
-    }
-
-    /**
-     * Delete the selected cell, shifting cells over to the left.
-     */
-    public void deleteCellShiftLeft() {
-        // All we do is copy the text from every cell in this row over.
-        for (int i = selectedColumn + 1; i < columns.size(); i++) {
-            setCellText(i - 1, selectedRow, getCellText(i, selectedRow));
-        }
-        setCellText(columns.size() - 1, selectedRow, "");
-    }
-
-    /**
-     * Delete the selected cell, shifting cells from below up.
-     */
-    public void deleteCellShiftUp() {
-        // All we do is copy the text from every cell in this column up.
-        for (int i = selectedRow + 1; i < rows.size(); i++) {
-            setCellText(selectedColumn, i - 1, getCellText(selectedColumn, i));
-        }
-        setCellText(selectedColumn, rows.size() - 1, "");
-    }
-
-    /**
-     * Set a particular cell read-only (non-editable) or not.
-     *
-     * @param column the cell column
-     * @param row the cell row
-     * @param readOnly if true, the cell will be non-editable
-     */
-    public void setCellReadOnly(final int column, final int row,
-        final boolean readOnly) {
-
-        if ((column < 0) || (column > columns.size() - 1)) {
-            throw new IndexOutOfBoundsException("Column count is " +
-                columns.size() + ", requested index " + column);
-        }
-        if ((row < 0) || (row > rows.size() - 1)) {
-            throw new IndexOutOfBoundsException("Row count is " +
-                rows.size() + ", requested index " + row);
-        }
-        rows.get(row).get(column).setReadOnly(readOnly);
-    }
-
-    /**
-     * Set an entire row of cells read-only (non-editable) or not.
-     *
-     * @param row the row number
-     * @param readOnly if true, the cells will be non-editable
-     */
-    public void setRowReadOnly(final int row, final boolean readOnly) {
-        if ((row < 0) || (row > rows.size() - 1)) {
-            throw new IndexOutOfBoundsException("Row count is " +
-                rows.size() + ", requested index " + row);
-        }
-        for (Cell cell: rows.get(row).cells) {
-            cell.setReadOnly(readOnly);
-        }
-    }
-
-    /**
-     * Set an entire column of cells read-only (non-editable) or not.
-     *
-     * @param column the column number
-     * @param readOnly if true, the cells will be non-editable
-     */
-    public void setColumnReadOnly(final int column, final boolean readOnly) {
-        if ((column < 0) || (column > columns.size() - 1)) {
-            throw new IndexOutOfBoundsException("Column count is " +
-                columns.size() + ", requested index " + column);
-        }
-        for (Cell cell: columns.get(column).cells) {
-            cell.setReadOnly(readOnly);
-        }
-    }
-
-    /**
-     * Set all borders across the entire table to Border.NONE.
-     */
-    public void setBorderAllNone() {
-        topBorder = Border.NONE;
-        leftBorder = Border.NONE;
-        for (int i = 0; i < columns.size(); i++) {
-            columns.get(i).rightBorder = Border.NONE;
-        }
-        for (int i = 0; i < rows.size(); i++) {
-            rows.get(i).bottomBorder = Border.NONE;
-            rows.get(i).height = 1;
-        }
-        bottomRightCorner();
-    }
-
-    /**
-     * Set all borders across the entire table to Border.SINGLE.
-     */
-    public void setBorderAllSingle() {
-        topBorder = Border.SINGLE;
-        leftBorder = Border.SINGLE;
-        for (int i = 0; i < columns.size(); i++) {
-            columns.get(i).rightBorder = Border.SINGLE;
-        }
-        for (int i = 0; i < rows.size(); i++) {
-            rows.get(i).bottomBorder = Border.SINGLE;
-            rows.get(i).height = 2;
-        }
-        alignGrid();
-    }
-
-    /**
-     * Set all borders around the selected cell to Border.NONE.
-     */
-    public void setBorderCellNone() {
-        if (selectedRow == 0) {
-            topBorder = Border.NONE;
-        }
-        if (selectedColumn == 0) {
-            leftBorder = Border.NONE;
-        }
-        if (selectedColumn > 0) {
-            columns.get(selectedColumn - 1).rightBorder = Border.NONE;
-        }
-        columns.get(selectedColumn).rightBorder = Border.NONE;
-        if (selectedRow > 0) {
-            rows.get(selectedRow - 1).bottomBorder = Border.NONE;
-            rows.get(selectedRow - 1).height = 1;
-        }
-        rows.get(selectedRow).bottomBorder = Border.NONE;
-        rows.get(selectedRow).height = 1;
-        bottomRightCorner();
-    }
-
-    /**
-     * Set all borders around the selected cell to Border.SINGLE.
-     */
-    public void setBorderCellSingle() {
-        if (selectedRow == 0) {
-            topBorder = Border.SINGLE;
-        }
-        if (selectedColumn == 0) {
-            leftBorder = Border.SINGLE;
-        }
-        if (selectedColumn > 0) {
-            columns.get(selectedColumn - 1).rightBorder = Border.SINGLE;
-        }
-        columns.get(selectedColumn).rightBorder = Border.SINGLE;
-        if (selectedRow > 0) {
-            rows.get(selectedRow - 1).bottomBorder = Border.SINGLE;
-            rows.get(selectedRow - 1).height = 2;
-        }
-        rows.get(selectedRow).bottomBorder = Border.SINGLE;
-        rows.get(selectedRow).height = 2;
-        alignGrid();
-    }
-
-    /**
-     * Set the column border to the right of the selected cell to
-     * Border.SINGLE.
-     */
-    public void setBorderColumnRightSingle() {
-        columns.get(selectedColumn).rightBorder = Border.SINGLE;
-        alignGrid();
-    }
-
-    /**
-     * Set the column border to the right of the selected cell to
-     * Border.SINGLE.
-     */
-    public void setBorderColumnLeftSingle() {
-        if (selectedColumn == 0) {
-            leftBorder = Border.SINGLE;
-        } else {
-            columns.get(selectedColumn - 1).rightBorder = Border.SINGLE;
-        }
-        alignGrid();
-    }
-
-    /**
-     * Set the row border above the selected cell to Border.SINGLE.
-     */
-    public void setBorderRowAboveSingle() {
-        if (selectedRow == 0) {
-            topBorder = Border.SINGLE;
-        } else {
-            rows.get(selectedRow - 1).bottomBorder = Border.SINGLE;
-            rows.get(selectedRow - 1).height = 2;
-        }
-        alignGrid();
-    }
-
-    /**
-     * Set the row border below the selected cell to Border.SINGLE.
-     */
-    public void setBorderRowBelowSingle() {
-        rows.get(selectedRow).bottomBorder = Border.SINGLE;
-        rows.get(selectedRow).height = 2;
-        alignGrid();
-    }
-
-    /**
-     * Set the row border below the selected cell to Border.DOUBLE.
-     */
-    public void setBorderRowBelowDouble() {
-        rows.get(selectedRow).bottomBorder = Border.DOUBLE;
-        rows.get(selectedRow).height = 2;
-        alignGrid();
-    }
-
-    /**
-     * Set the row border below the selected cell to Border.THICK.
-     */
-    public void setBorderRowBelowThick() {
-        rows.get(selectedRow).bottomBorder = Border.THICK;
-        rows.get(selectedRow).height = 2;
-        alignGrid();
-    }
-
-}