/*
* This file is part of lanterna (http://code.google.com/p/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 <http://www.gnu.org/licenses/>.
*
* Copyright (C) 2010-2017 Martin Berglund
*/
package com.googlecode.lanterna.gui2.table;
import com.googlecode.lanterna.gui2.*;
import com.googlecode.lanterna.input.KeyStroke;
import java.util.List;
/**
* 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 <V> Type of data to store in the table cells, presented through {@code toString()}
* @author Martin
*/
public class Table<V> extends AbstractInteractableComponent<Table<V>> {
private TableModel<V> tableModel;
private TableModel.Listener<V> tableModelListener; // Used to invalidate the table whenever the model changes
private TableHeaderRenderer<V> tableHeaderRenderer;
private TableCellRenderer<V> tableCellRenderer;
private Runnable selectAction;
private boolean cellSelection;
private int visibleRows;
private int visibleColumns;
private int viewTopRow;
private int viewLeftColumn;
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<V>();
this.tableCellRenderer = new DefaultTableCellRenderer<V>();
this.tableModel = new TableModel<V>(columnLabels);
this.selectAction = null;
this.visibleColumns = 0;
this.visibleRows = 0;
this.viewTopRow = 0;
this.viewLeftColumn = 0;
this.cellSelection = false;
this.selectedRow = 0;
this.selectedColumn = -1;
this.escapeByArrowKey = true;
this.tableModelListener = new TableModel.Listener<V>() {
@Override
public void onRowAdded(TableModel<V> model, int index) {
invalidate();
}
@Override
public void onRowRemoved(TableModel<V> model, int index, List<V> oldRow) {
invalidate();
}
@Override
public void onColumnAdded(TableModel<V> model, int index) {
invalidate();
}
@Override
public void onColumnRemoved(TableModel<V> model, int index, String oldHeader, List<V> oldColumn) {
invalidate();
}
@Override
public void onCellChanged(TableModel<V> 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<V> 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<V> setTableModel(TableModel<V> 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<V> 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<V> setTableCellRenderer(TableCellRenderer<V> 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<V> 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<V> setTableHeaderRenderer(TableHeaderRenderer<V> 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
*/
public int getViewTopRow() {
return viewTopRow;
}
/**
* 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
*/
public synchronized Table<V> setViewTopRow(int viewTopRow) {
this.viewTopRow = 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
*/
public int getViewLeftColumn() {
return viewLeftColumn;
}
/**
* 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
*/
public synchronized Table<V> setViewLeftColumn(int viewLeftColumn) {
this.viewLeftColumn = 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<V> setSelectedColumn(int selectedColumn) {
if(cellSelection) {
this.selectedColumn = selectedColumn;
ensureSelectedItemIsVisible();
}
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<V> setSelectedRow(int selectedRow) {
this.selectedRow = selectedRow;
ensureSelectedItemIsVisible();
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<V> 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 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 key
* @return Itself
*/
public synchronized Table<V> 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<V> setEscapeByArrowKey(boolean escapeByArrowKey) {
this.escapeByArrowKey = escapeByArrowKey;
return this;
}
@Override
protected TableRenderer<V> createDefaultRenderer() {
return new DefaultTableRenderer<V>();
}
@Override
public TableRenderer<V> getRenderer() {
return (TableRenderer<V>)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 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 Enter:
Runnable runnable = selectAction; //To avoid synchronizing
if(runnable != null) {
runnable.run();
}
else {
return Result.MOVE_FOCUS_NEXT;
}
break;
default:
return super.handleKeyStroke(keyStroke);
}
ensureSelectedItemIsVisible();
invalidate();
return Result.HANDLED;
}
private void ensureSelectedItemIsVisible() {
if(visibleRows > 0 && selectedRow < viewTopRow) {
viewTopRow = selectedRow;
}
else if(visibleRows > 0 && selectedRow >= viewTopRow + visibleRows) {
viewTopRow = Math.max(0, selectedRow - visibleRows + 1);
}
if(selectedColumn != -1) {
if(visibleColumns > 0 && selectedColumn < viewLeftColumn) {
viewLeftColumn = selectedColumn;
}
else if(visibleColumns > 0 && selectedColumn >= viewLeftColumn + visibleColumns) {
viewLeftColumn = Math.max(0, selectedColumn - visibleColumns + 1);
}
}
}
}