All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.googlecode.lanterna.gui2.table.Table Maven / Gradle / Ivy

There is a newer version: 3.2.0-alpha1
Show newest version
/*
 * This file is part of lanterna (https://github.com/mabe02/lanterna).
 *
 * lanterna is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see .
 *
 * Copyright (C) 2010-2020 Martin Berglund
 */
package com.googlecode.lanterna.gui2.table;

import java.util.List;

import com.googlecode.lanterna.TerminalPosition;
import com.googlecode.lanterna.gui2.AbstractInteractableComponent;
import com.googlecode.lanterna.input.KeyStroke;
import com.googlecode.lanterna.input.KeyType;
import com.googlecode.lanterna.input.MouseAction;
import com.googlecode.lanterna.input.MouseActionType;

/**
 * The table class is an interactable component that displays a grid of cells containing data along with a header of
 * labels. It supports scrolling when the number of rows and/or columns gets too large to fit and also supports
 * user selection which is either row-based or cell-based. User will move the current selection by using the arrow keys
 * on the keyboard.
 * @param  Type of data to store in the table cells, presented through {@code toString()}
 * @author Martin
 */
public class Table extends AbstractInteractableComponent> {
    private TableModel tableModel;
    private TableModel.Listener tableModelListener;  // Used to invalidate the table whenever the model changes
    private TableHeaderRenderer tableHeaderRenderer;
    private TableCellRenderer tableCellRenderer;
    private Runnable selectAction;
    private boolean cellSelection;
    private int visibleRows;
    private int visibleColumns;
    private int selectedRow;
    private int selectedColumn;
    private boolean escapeByArrowKey;

    /**
     * Creates a new {@code Table} with the number of columns as specified by the array of labels
     * @param columnLabels Creates one column per label in the array, must be more than one
     */
    public Table(String... columnLabels) {
        if(columnLabels.length == 0) {
            throw new IllegalArgumentException("Table needs at least one column");
        }
        this.tableHeaderRenderer = new DefaultTableHeaderRenderer<>();
        this.tableCellRenderer = new DefaultTableCellRenderer<>();
        this.tableModel = new TableModel<>(columnLabels);
        this.selectAction = null;
        this.visibleColumns = 0;
        this.visibleRows = 0;
        this.cellSelection = false;
        this.selectedRow = 0;
        this.selectedColumn = -1;
        this.escapeByArrowKey = true;

        this.tableModelListener = new TableModel.Listener() {
            @Override
            public void onRowAdded(TableModel model, int index) {
                if (index <= selectedRow) {
                    selectedRow = Math.min(model.getRowCount() - 1, selectedRow + 1);
                }
                invalidate();
            }

            @Override
            public void onRowRemoved(TableModel model, int index, List oldRow) {
                if (index < selectedRow) {
                    selectedRow = Math.max(0, selectedRow-1);
                } else {
                    // We may have deleted the selected row
                    int rowCount = model.getRowCount();
                    if (selectedRow > rowCount - 1) {
                        selectedRow = Math.max(0, rowCount - 1);
                    }
                }
                invalidate();
            }

            @Override
            public void onColumnAdded(TableModel model, int index) {
                invalidate();
            }

            @Override
            public void onColumnRemoved(TableModel model, int index, String oldHeader, List oldColumn) {
                invalidate();
            }

            @Override
            public void onCellChanged(TableModel model, int row, int column, V oldValue, V newValue) {
                invalidate();
            }
        };
        this.tableModel.addListener(tableModelListener);
    }

    /**
     * Returns the underlying table model
     * @return Underlying table model
     */
    public TableModel getTableModel() {
        return tableModel;
    }

    /**
     * Updates the table with a new table model, effectively replacing the content of the table completely
     * @param tableModel New table model
     * @return Itself
     */
    public synchronized Table setTableModel(TableModel tableModel) {
        if(tableModel == null) {
            throw new IllegalArgumentException("Cannot assign a null TableModel");
        }
        this.tableModel.removeListener(tableModelListener);
        this.tableModel = tableModel;
        this.tableModel.addListener(tableModelListener);
        invalidate();
        return this;
    }

    /**
     * Returns the {@code TableCellRenderer} used by this table when drawing cells
     * @return {@code TableCellRenderer} used by this table when drawing cells
     */
    public TableCellRenderer getTableCellRenderer() {
        return tableCellRenderer;
    }

    /**
     * Replaces the {@code TableCellRenderer} used by this table when drawing cells
     * @param tableCellRenderer New {@code TableCellRenderer} to use
     * @return Itself
     */
    public synchronized Table setTableCellRenderer(TableCellRenderer tableCellRenderer) {
        this.tableCellRenderer = tableCellRenderer;
        invalidate();
        return this;
    }

    /**
     * Returns the {@code TableHeaderRenderer} used by this table when drawing the table's header
     * @return {@code TableHeaderRenderer} used by this table when drawing the table's header
     */
    public TableHeaderRenderer getTableHeaderRenderer() {
        return tableHeaderRenderer;
    }

    /**
     * Replaces the {@code TableHeaderRenderer} used by this table when drawing the table's header
     * @param tableHeaderRenderer New {@code TableHeaderRenderer} to use
     * @return Itself
     */
    public synchronized Table setTableHeaderRenderer(TableHeaderRenderer tableHeaderRenderer) {
        this.tableHeaderRenderer = tableHeaderRenderer;
        invalidate();
        return this;
    }

    /**
     * Sets the number of columns this table should show. If there are more columns in the table model, a scrollbar will
     * be used to allow the user to scroll left and right and view all columns.
     * @param visibleColumns Number of columns to display at once
     */
    public synchronized void setVisibleColumns(int visibleColumns) {
        this.visibleColumns = visibleColumns;
        invalidate();
    }

    /**
     * Returns the number of columns this table will show. If there are more columns in the table model, a scrollbar
     * will be used to allow the user to scroll left and right and view all columns.
     * @return Number of visible columns for this table
     */
    public int getVisibleColumns() {
        return visibleColumns;
    }

    /**
     * Sets the number of rows this table will show. If there are more rows in the table model, a scrollbar will be used
     * to allow the user to scroll up and down and view all rows.
     * @param visibleRows Number of rows to display at once
     */
    public synchronized void setVisibleRows(int visibleRows) {
        this.visibleRows = visibleRows;
        invalidate();
    }

    /**
     * Returns the number of rows this table will show. If there are more rows in the table model, a scrollbar will be
     * used to allow the user to scroll up and down and view all rows.
     * @return Number of rows to display at once
     */
    public int getVisibleRows() {
        return visibleRows;
    }

    /**
     * Returns the index of the row that is currently the first row visible. This is always 0 unless scrolling has been
     * enabled and either the user or the software (through {@code setViewTopRow(..)}) has scrolled down.
     * @return Index of the row that is currently the first row visible
     * @deprecated Use the table renderers method instead
     */
    @Deprecated
    public int getViewTopRow() {
        return getRenderer().getViewTopRow();
    }
    
    /**
     * Returns the index of the first row that is currently visible.
     * @return the index of the first row that is currently visible
     */
    public int getFirstViewedRowIndex() {
        return getRenderer().getViewTopRow();
    }
    
    /**
     * Returns the index of the last row that is currently visible.
     * @return the index of the last row that is currently visible
     */
    public int getLastViewedRowIndex() {
        int visibleRows = getRenderer().getVisibleRowsOnLastDraw();
        return Math.min(getRenderer().getViewTopRow() + visibleRows -1, tableModel.getRowCount() -1);
    }

    /**
     * Sets the view row offset for the first row to display in the table. Calling this with 0 will make the first row
     * in the model be the first visible row in the table.
     *
     * @param viewTopRow Index of the row that is currently the first row visible
     * @return Itself
     * @deprecated Use the table renderers method instead
     */
    @Deprecated
    public synchronized Table setViewTopRow(int viewTopRow) {
        getRenderer().setViewTopRow(viewTopRow);
        return this;
    }

    /**
     * Returns the index of the column that is currently the first column visible. This is always 0 unless scrolling has
     * been enabled and either the user or the software (through {@code setViewLeftColumn(..)}) has scrolled to the
     * right.
     * @return Index of the column that is currently the first column visible
     * @deprecated Use the table renderers method instead
     */
    @Deprecated
    public int getViewLeftColumn() {
        return getRenderer().getViewLeftColumn();
    }

    /**
     * Sets the view column offset for the first column to display in the table. Calling this with 0 will make the first
     * column in the model be the first visible column in the table.
     *
     * @param viewLeftColumn Index of the column that is currently the first column visible
     * @return Itself
     * @deprecated Use the table renderers method instead
     */
    @Deprecated
    public synchronized Table setViewLeftColumn(int viewLeftColumn) {
        getRenderer().setViewLeftColumn(viewLeftColumn);
        return this;
    }

    /**
     * Returns the currently selection column index, if in cell-selection mode. Otherwise it returns -1.
     * @return In cell-selection mode returns the index of the selected column, otherwise -1
     */
    public int getSelectedColumn() {
        return selectedColumn;
    }

    /**
     * If in cell selection mode, updates which column is selected and ensures the selected column is visible in the
     * view. If not in cell selection mode, does nothing.
     * @param selectedColumn Index of the column that should be selected
     * @return Itself
     */
    public synchronized Table setSelectedColumn(int selectedColumn) {
        if(cellSelection) {
            this.selectedColumn = selectedColumn;
        }
        return this;
    }

    /**
     * Returns the index of the currently selected row
     * @return Index of the currently selected row
     */
    public int getSelectedRow() {
        return selectedRow;
    }

    /**
     * Sets the index of the selected row and ensures the selected row is visible in the view
     * @param selectedRow Index of the row to select
     * @return Itself
     */
    public synchronized Table setSelectedRow(int selectedRow) {
        if (selectedRow < 0) {
            throw new IllegalArgumentException("selectedRow must be >= 0 but was " + selectedRow);
        }
        int rowCount = tableModel.getRowCount();
        if (rowCount == 0) {
            selectedRow = 0;
        } else if (selectedRow > rowCount - 1) {
            selectedRow = rowCount - 1;
        }
        this.selectedRow = selectedRow;
        return this;
    }

    /**
     * If {@code true}, the user will be able to select and navigate individual cells, otherwise the user can only
     * select full rows.
     * @param cellSelection {@code true} if cell selection should be enabled, {@code false} for row selection
     * @return Itself
     */
    public synchronized Table setCellSelection(boolean cellSelection) {
        this.cellSelection = cellSelection;
        if(cellSelection && selectedColumn == -1) {
            selectedColumn = 0;
        }
        else if(!cellSelection) {
            selectedColumn = -1;
        }
        return this;
    }

    /**
     * Returns {@code true} if this table is in cell-selection mode, otherwise {@code false}
     * @return {@code true} if this table is in cell-selection mode, otherwise {@code false}
     */
    public boolean isCellSelection() {
        return cellSelection;
    }

    /**
     * Assigns an action to run whenever the user presses the enter or space key while focused on the table. If called with
     * {@code null}, no action will be run.
     * @param selectAction Action to perform when user presses the enter or space key
     * @return Itself
     */
    public synchronized Table setSelectAction(Runnable selectAction) {
        this.selectAction = selectAction;
        return this;
    }

    /**
     * Returns {@code true} if this table can be navigated away from when the selected row is at one of the extremes and
     * the user presses the array key to continue in that direction. With {@code escapeByArrowKey} set to {@code true},
     * this will move focus away from the table in the direction the user pressed, if {@code false} then nothing will
     * happen.
     * @return {@code true} if user can switch focus away from the table using arrow keys, {@code false} otherwise
     */
    public boolean isEscapeByArrowKey() {
        return escapeByArrowKey;
    }

    /**
     * Sets the flag for if this table can be navigated away from when the selected row is at one of the extremes and
     * the user presses the array key to continue in that direction. With {@code escapeByArrowKey} set to {@code true},
     * this will move focus away from the table in the direction the user pressed, if {@code false} then nothing will
     * happen.
     * @param escapeByArrowKey {@code true} if user can switch focus away from the table using arrow keys, {@code false} otherwise
     * @return Itself
     */
    public synchronized Table setEscapeByArrowKey(boolean escapeByArrowKey) {
        this.escapeByArrowKey = escapeByArrowKey;
        return this;
    }

    @Override
    protected TableRenderer createDefaultRenderer() {
        return new DefaultTableRenderer<>();
    }

    @Override
    public TableRenderer getRenderer() {
        return (TableRenderer)super.getRenderer();
    }

    @Override
    public Result handleKeyStroke(KeyStroke keyStroke) {
        switch(keyStroke.getKeyType()) {
            case ArrowUp:
                if(selectedRow > 0) {
                    selectedRow--;
                }
                else if(escapeByArrowKey) {
                    return Result.MOVE_FOCUS_UP;
                }
                break;
            case ArrowDown:
                if(selectedRow < tableModel.getRowCount() - 1) {
                    selectedRow++;
                }
                else if(escapeByArrowKey) {
                    return Result.MOVE_FOCUS_DOWN;
                }
                break;
            case PageUp:
                if(getRenderer().getVisibleRowsOnLastDraw() > 0 && selectedRow > 0) {
                    selectedRow -= Math.min(getRenderer().getVisibleRowsOnLastDraw() - 1, selectedRow);
                }
                break;
            case PageDown:
                if(getRenderer().getVisibleRowsOnLastDraw() > 0 && selectedRow < tableModel.getRowCount() - 1) {
                    int toEndDistance = tableModel.getRowCount() - 1 - selectedRow;
                    selectedRow += Math.min(getRenderer().getVisibleRowsOnLastDraw() - 1, toEndDistance);
                }
                break;
            case Home:
                selectedRow = 0;
                break;
            case End:
                selectedRow = tableModel.getRowCount() - 1;
                break;
            case ArrowLeft:
                if(cellSelection && selectedColumn > 0) {
                    selectedColumn--;
                }
                else if(escapeByArrowKey) {
                    return Result.MOVE_FOCUS_LEFT;
                }
                break;
            case ArrowRight:
                if(cellSelection && selectedColumn < tableModel.getColumnCount() - 1) {
                    selectedColumn++;
                }
                else if(escapeByArrowKey) {
                    return Result.MOVE_FOCUS_RIGHT;
                }
                break;
            case Character:
            case Enter:
                if (isKeyboardActivationStroke(keyStroke)) {
                    Runnable runnable = selectAction;   //To avoid synchronizing
                    if(runnable != null) {
                        runnable.run();
                    } else {
                        return Result.HANDLED;
                    }
                    break;
                } else {
                    return super.handleKeyStroke(keyStroke);
                }
            case MouseEvent:
                MouseAction action = (MouseAction)keyStroke;
                MouseActionType actionType = action.getActionType();
                if (actionType == MouseActionType.MOVE) {
                    // do nothing
                    return Result.UNHANDLED;
                } 
                if (!isFocused()) {
                    super.handleKeyStroke(keyStroke);
                }
                int mouseRow = getRowByMouseAction((MouseAction) keyStroke);
                int mouseColumn = getColumnByMouseAction((MouseAction) keyStroke);
                boolean isDifferentCell = mouseRow != selectedRow || mouseColumn != selectedColumn;
                selectedRow = mouseRow;
                selectedColumn = mouseColumn;
                if (isDifferentCell) {
                    return handleKeyStroke(new KeyStroke(KeyType.Enter));
                }
                break;
            default:
                return super.handleKeyStroke(keyStroke);
        }
        invalidate();
        return Result.HANDLED;
    }
    
    /**
     * By converting {@link TerminalPosition}s to
     * {@link #toGlobal(TerminalPosition)} gets row clicked on by mouse action.
     * 
     * @return row of a table that was clicked on with {@link MouseAction}
     */
    protected int getRowByMouseAction(MouseAction mouseAction) {
        int minPossible = getFirstViewedRowIndex();
        int maxPossible = getLastViewedRowIndex();
        int mouseSpecified = mouseAction.getPosition().getRow() - getGlobalPosition().getRow() - 1;
        
        return Math.max(minPossible, Math.min(mouseSpecified, maxPossible));
    }
    
    /**
     * By converting {@link TerminalPosition}s to
     * {@link #toGlobal(TerminalPosition)} and by comparing widths of column
     * headers, gets column clicked on by mouse action.
     * 
     * @return row of a table that was clicked on with {@link MouseAction}
     */
    protected int getColumnByMouseAction(MouseAction mouseAction) {
        int maxColumnIndex = tableModel.getColumnCount() -1;
        int column = 0;
        int columnSize = tableHeaderRenderer.getPreferredSize(this, tableModel.getColumnLabel(column), column).getColumns();
        int globalColumnMoused = mouseAction.getPosition().getColumn() - getGlobalPosition().getColumn();
        while (globalColumnMoused - columnSize - 1 >= 0 && column < maxColumnIndex) {
            globalColumnMoused -= columnSize;
            column++;
            columnSize = tableHeaderRenderer.getPreferredSize(this, tableModel.getColumnLabel(column), column).getColumns();
        }
        return column;
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy