/* * 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.*; import com.googlecode.lanterna.graphics.Theme; import com.googlecode.lanterna.graphics.ThemeDefinition; import com.googlecode.lanterna.gui2.Direction; import com.googlecode.lanterna.gui2.ScrollBar; import com.googlecode.lanterna.gui2.TextGUIGraphics; import java.util.ArrayList; import java.util.List; /** * Default implementation of {@code TableRenderer} * @param <V> Type of data stored in each table cell * @author Martin */ public class DefaultTableRenderer<V> implements TableRenderer<V> { private final ScrollBar verticalScrollBar; private final ScrollBar horizontalScrollBar; private TableCellBorderStyle headerVerticalBorderStyle; private TableCellBorderStyle headerHorizontalBorderStyle; private TableCellBorderStyle cellVerticalBorderStyle; private TableCellBorderStyle cellHorizontalBorderStyle; //So that we don't have to recalculate the size every time. This still isn't optimal but shouganai. private TerminalSize cachedSize; private final List<Integer> columnSizes; private final List<Integer> rowSizes; private int headerSizeInRows; /** * Default constructor */ public DefaultTableRenderer() { verticalScrollBar = new ScrollBar(Direction.VERTICAL); horizontalScrollBar = new ScrollBar(Direction.HORIZONTAL); headerVerticalBorderStyle = TableCellBorderStyle.None; headerHorizontalBorderStyle = TableCellBorderStyle.EmptySpace; cellVerticalBorderStyle = TableCellBorderStyle.None; cellHorizontalBorderStyle = TableCellBorderStyle.EmptySpace; cachedSize = null; columnSizes = new ArrayList<Integer>(); rowSizes = new ArrayList<Integer>(); headerSizeInRows = 0; } /** * Sets the style to be used when separating the table header row from the actual "data" cells below. This will * cause a new line to be added under the header labels, unless set to {@code TableCellBorderStyle.None}. * * @param headerVerticalBorderStyle Style to use to separate Table header from body */ public void setHeaderVerticalBorderStyle(TableCellBorderStyle headerVerticalBorderStyle) { this.headerVerticalBorderStyle = headerVerticalBorderStyle; } /** * Sets the style to be used when separating the table header labels from each other. This will cause a new * column to be added in between each label, unless set to {@code TableCellBorderStyle.None}. * * @param headerHorizontalBorderStyle Style to use when separating header columns horizontally */ public void setHeaderHorizontalBorderStyle(TableCellBorderStyle headerHorizontalBorderStyle) { this.headerHorizontalBorderStyle = headerHorizontalBorderStyle; } /** * Sets the style to be used when vertically separating table cells from each other. This will cause a new line * to be added between every row, unless set to {@code TableCellBorderStyle.None}. * * @param cellVerticalBorderStyle Style to use to separate table cells vertically */ public void setCellVerticalBorderStyle(TableCellBorderStyle cellVerticalBorderStyle) { this.cellVerticalBorderStyle = cellVerticalBorderStyle; } /** * Sets the style to be used when horizontally separating table cells from each other. This will cause a new * column to be added between every row, unless set to {@code TableCellBorderStyle.None}. * * @param cellHorizontalBorderStyle Style to use to separate table cells horizontally */ public void setCellHorizontalBorderStyle(TableCellBorderStyle cellHorizontalBorderStyle) { this.cellHorizontalBorderStyle = cellHorizontalBorderStyle; } private boolean isHorizontallySpaced() { return headerHorizontalBorderStyle != TableCellBorderStyle.None || cellHorizontalBorderStyle != TableCellBorderStyle.None; } @Override public TerminalSize getPreferredSize(Table<V> table) { //Quick bypass if the table hasn't changed if(!table.isInvalid() && cachedSize != null) { return cachedSize; } TableModel<V> tableModel = table.getTableModel(); int viewLeftColumn = table.getViewLeftColumn(); int viewTopRow = table.getViewTopRow(); int visibleColumns = table.getVisibleColumns(); int visibleRows = table.getVisibleRows(); List<List<V>> rows = tableModel.getRows(); List<String> columnHeaders = tableModel.getColumnLabels(); TableHeaderRenderer<V> tableHeaderRenderer = table.getTableHeaderRenderer(); TableCellRenderer<V> tableCellRenderer = table.getTableCellRenderer(); if(visibleColumns == 0) { visibleColumns = tableModel.getColumnCount(); } if(visibleRows == 0) { visibleRows = tableModel.getRowCount(); } columnSizes.clear(); rowSizes.clear(); if(tableModel.getColumnCount() == 0) { return TerminalSize.ZERO; } // If there are no rows, base the column sizes off of the column labels if(rows.size() == 0) { for(int columnIndex = viewLeftColumn; columnIndex < viewLeftColumn + visibleColumns; columnIndex++) { int columnSize = tableHeaderRenderer.getPreferredSize(table, columnHeaders.get(columnIndex), columnIndex).getColumns(); int listOffset = columnIndex - viewLeftColumn; if(columnSizes.size() == listOffset) { columnSizes.add(columnSize); } else { if(columnSizes.get(listOffset) < columnSize) { columnSizes.set(listOffset, columnSize); } } } } for(int rowIndex = 0; rowIndex < rows.size(); rowIndex++) { List<V> row = rows.get(rowIndex); for(int columnIndex = viewLeftColumn; columnIndex < Math.min(row.size(), viewLeftColumn + visibleColumns); columnIndex++) { V cell = row.get(columnIndex); int columnSize = tableCellRenderer.getPreferredSize(table, cell, columnIndex, rowIndex).getColumns(); int listOffset = columnIndex - viewLeftColumn; if(columnSizes.size() == listOffset) { columnSizes.add(columnSize); } else { if(columnSizes.get(listOffset) < columnSize) { columnSizes.set(listOffset, columnSize); } } } //Do the headers too, on the first iteration if(rowIndex == 0) { for(int columnIndex = viewLeftColumn; columnIndex < Math.min(row.size(), viewLeftColumn + visibleColumns); columnIndex++) { int columnSize = tableHeaderRenderer.getPreferredSize(table, columnHeaders.get(columnIndex), columnIndex).getColumns(); int listOffset = columnIndex - viewLeftColumn; if(columnSizes.size() == listOffset) { columnSizes.add(columnSize); } else { if(columnSizes.get(listOffset) < columnSize) { columnSizes.set(listOffset, columnSize); } } } } } for(int columnIndex = 0; columnIndex < columnHeaders.size(); columnIndex++) { for(int rowIndex = viewTopRow; rowIndex < Math.min(rows.size(), viewTopRow + visibleRows); rowIndex++) { V cell = rows.get(rowIndex).get(columnIndex); int rowSize = tableCellRenderer.getPreferredSize(table, cell, columnIndex, rowIndex).getRows(); int listOffset = rowIndex - viewTopRow; if(rowSizes.size() == listOffset) { rowSizes.add(rowSize); } else { if(rowSizes.get(listOffset) < rowSize) { rowSizes.set(listOffset, rowSize); } } } } int preferredRowSize = 0; int preferredColumnSize = 0; for(int size: columnSizes) { preferredColumnSize += size; } for(int size: rowSizes) { preferredRowSize += size; } headerSizeInRows = 0; for(int columnIndex = 0; columnIndex < columnHeaders.size(); columnIndex++) { int headerRows = tableHeaderRenderer.getPreferredSize(table, columnHeaders.get(columnIndex), columnIndex).getRows(); if(headerSizeInRows < headerRows) { headerSizeInRows = headerRows; } } preferredRowSize += headerSizeInRows; if(headerVerticalBorderStyle != TableCellBorderStyle.None) { preferredRowSize++; //Spacing between header and body } if(cellVerticalBorderStyle != TableCellBorderStyle.None) { if(!rows.isEmpty()) { preferredRowSize += Math.min(rows.size(), visibleRows) - 1; //Vertical space between cells } } if(isHorizontallySpaced()) { if(!columnHeaders.isEmpty()) { preferredColumnSize += Math.min(tableModel.getColumnCount(), visibleColumns) - 1; //Spacing between the columns } } //Add on space taken by scrollbars (if needed) if(visibleRows < rows.size()) { preferredColumnSize++; } if(visibleColumns < tableModel.getColumnCount()) { preferredRowSize++; } cachedSize = new TerminalSize(preferredColumnSize, preferredRowSize); return cachedSize; } @Override public TerminalPosition getCursorLocation(Table<V> component) { return null; } @Override public void drawComponent(TextGUIGraphics graphics, Table<V> table) { //Get the size TerminalSize area = graphics.getSize(); //Don't even bother if(area.getRows() == 0 || area.getColumns() == 0) { return; } // Get preferred size if the table model has changed if(table.isInvalid()) getPreferredSize(table); int topPosition = drawHeader(graphics, table); drawRows(graphics, table, topPosition); } private int drawHeader(TextGUIGraphics graphics, Table<V> table) { Theme theme = table.getTheme(); TableHeaderRenderer<V> tableHeaderRenderer = table.getTableHeaderRenderer(); List<String> headers = table.getTableModel().getColumnLabels(); int viewLeftColumn = table.getViewLeftColumn(); int visibleColumns = table.getVisibleColumns(); if(visibleColumns == 0) { visibleColumns = table.getTableModel().getColumnCount(); } int topPosition = 0; int leftPosition = 0; int endColumnIndex = Math.min(headers.size(), viewLeftColumn + visibleColumns); for(int index = viewLeftColumn; index < endColumnIndex; index++) { String label = headers.get(index); TerminalSize size = new TerminalSize(columnSizes.get(index - viewLeftColumn), headerSizeInRows); tableHeaderRenderer.drawHeader(table, label, index, graphics.newTextGraphics(new TerminalPosition(leftPosition, 0), size)); leftPosition += size.getColumns(); if(headerHorizontalBorderStyle != TableCellBorderStyle.None && index < (endColumnIndex - 1)) { graphics.applyThemeStyle(theme.getDefinition(Table.class).getNormal()); graphics.setCharacter(leftPosition, 0, getVerticalCharacter(headerHorizontalBorderStyle)); leftPosition++; } } topPosition += headerSizeInRows; if(headerVerticalBorderStyle != TableCellBorderStyle.None) { leftPosition = 0; graphics.applyThemeStyle(theme.getDefinition(Table.class).getNormal()); for(int i = 0; i < columnSizes.size(); i++) { if(i > 0) { graphics.setCharacter( leftPosition, topPosition, getJunctionCharacter( headerVerticalBorderStyle, headerHorizontalBorderStyle, cellHorizontalBorderStyle)); leftPosition++; } int columnWidth = columnSizes.get(i); graphics.drawLine(leftPosition, topPosition, leftPosition + columnWidth - 1, topPosition, getHorizontalCharacter(headerVerticalBorderStyle)); leftPosition += columnWidth; } //Expand out the line in case the area is bigger if(leftPosition < graphics.getSize().getColumns()) { graphics.drawLine(leftPosition, topPosition, graphics.getSize().getColumns() - 1, topPosition, getHorizontalCharacter(headerVerticalBorderStyle)); } topPosition++; } return topPosition; } private void drawRows(TextGUIGraphics graphics, Table<V> table, int topPosition) { Theme theme = table.getTheme(); ThemeDefinition themeDefinition = theme.getDefinition(Table.class); TerminalSize area = graphics.getSize(); TableCellRenderer<V> tableCellRenderer = table.getTableCellRenderer(); TableModel<V> tableModel = table.getTableModel(); List<List<V>> rows = tableModel.getRows(); int viewTopRow = table.getViewTopRow(); int viewLeftColumn = table.getViewLeftColumn(); int visibleRows = table.getVisibleRows(); int visibleColumns = table.getVisibleColumns(); if(visibleColumns == 0) { visibleColumns = tableModel.getColumnCount(); } if(visibleRows == 0) { visibleRows = tableModel.getRowCount(); } //Draw scrollbars (if needed) if(visibleRows < rows.size()) { TerminalSize verticalScrollBarPreferredSize = verticalScrollBar.getPreferredSize(); int scrollBarHeight = graphics.getSize().getRows() - topPosition; if(visibleColumns < tableModel.getColumnCount()) { scrollBarHeight--; } verticalScrollBar.setPosition(new TerminalPosition(graphics.getSize().getColumns() - verticalScrollBarPreferredSize.getColumns(), topPosition)); verticalScrollBar.setSize(verticalScrollBarPreferredSize.withRows(scrollBarHeight)); verticalScrollBar.setScrollMaximum(rows.size()); verticalScrollBar.setViewSize(visibleRows); verticalScrollBar.setScrollPosition(viewTopRow); // Ensure the parent is correct if(table.getParent() != verticalScrollBar.getParent()) { if(verticalScrollBar.getParent() != null) { verticalScrollBar.onRemoved(verticalScrollBar.getParent()); } if(table.getParent() != null) { verticalScrollBar.onAdded(table.getParent()); } } // Finally draw the thing verticalScrollBar.draw(graphics.newTextGraphics(verticalScrollBar.getPosition(), verticalScrollBar.getSize())); // Adjust graphics object to the remaining area when the vertical scrollbar is subtracted graphics = graphics.newTextGraphics(TerminalPosition.TOP_LEFT_CORNER, graphics.getSize().withRelativeColumns(-verticalScrollBarPreferredSize.getColumns())); } if(visibleColumns < tableModel.getColumnCount()) { TerminalSize horizontalScrollBarPreferredSize = horizontalScrollBar.getPreferredSize(); int scrollBarWidth = graphics.getSize().getColumns(); horizontalScrollBar.setPosition(new TerminalPosition(0, graphics.getSize().getRows() - horizontalScrollBarPreferredSize.getRows())); horizontalScrollBar.setSize(horizontalScrollBarPreferredSize.withColumns(scrollBarWidth)); horizontalScrollBar.setScrollMaximum(tableModel.getColumnCount()); horizontalScrollBar.setViewSize(visibleColumns); horizontalScrollBar.setScrollPosition(viewLeftColumn); // Ensure the parent is correct if(table.getParent() != horizontalScrollBar.getParent()) { if(horizontalScrollBar.getParent() != null) { horizontalScrollBar.onRemoved(horizontalScrollBar.getParent()); } if(table.getParent() != null) { horizontalScrollBar.onAdded(table.getParent()); } } // Finally draw the thing horizontalScrollBar.draw(graphics.newTextGraphics(horizontalScrollBar.getPosition(), horizontalScrollBar.getSize())); // Adjust graphics object to the remaining area when the horizontal scrollbar is subtracted graphics = graphics.newTextGraphics(TerminalPosition.TOP_LEFT_CORNER, graphics.getSize().withRelativeRows(-horizontalScrollBarPreferredSize.getRows())); } int leftPosition; for(int rowIndex = viewTopRow; rowIndex < Math.min(viewTopRow + visibleRows, rows.size()); rowIndex++) { leftPosition = 0; List<V> row = rows.get(rowIndex); for(int columnIndex = viewLeftColumn; columnIndex < Math.min(viewLeftColumn + visibleColumns, row.size()); columnIndex++) { if(columnIndex > viewLeftColumn) { if(table.getSelectedRow() == rowIndex && !table.isCellSelection()) { if(table.isFocused()) { graphics.applyThemeStyle(themeDefinition.getActive()); } else { graphics.applyThemeStyle(themeDefinition.getSelected()); } } else { graphics.applyThemeStyle(themeDefinition.getNormal()); } graphics.setCharacter(leftPosition, topPosition, getVerticalCharacter(cellHorizontalBorderStyle)); leftPosition++; } V cell = row.get(columnIndex); TerminalPosition cellPosition = new TerminalPosition(leftPosition, topPosition); TerminalSize cellArea = new TerminalSize(columnSizes.get(columnIndex - viewLeftColumn), rowSizes.get(rowIndex - viewTopRow)); tableCellRenderer.drawCell(table, cell, columnIndex, rowIndex, graphics.newTextGraphics(cellPosition, cellArea)); leftPosition += cellArea.getColumns(); if(leftPosition > area.getColumns()) { break; } } topPosition += rowSizes.get(rowIndex - viewTopRow); if(cellVerticalBorderStyle != TableCellBorderStyle.None) { leftPosition = 0; graphics.applyThemeStyle(themeDefinition.getNormal()); for(int i = 0; i < columnSizes.size(); i++) { if(i > 0) { graphics.setCharacter( leftPosition, topPosition, getJunctionCharacter( cellVerticalBorderStyle, cellHorizontalBorderStyle, cellHorizontalBorderStyle)); leftPosition++; } int columnWidth = columnSizes.get(i); graphics.drawLine(leftPosition, topPosition, leftPosition + columnWidth - 1, topPosition, getHorizontalCharacter(cellVerticalBorderStyle)); leftPosition += columnWidth; } topPosition += 1; } if(topPosition > area.getRows()) { break; } } } private char getHorizontalCharacter(TableCellBorderStyle style) { switch(style) { case SingleLine: return Symbols.SINGLE_LINE_HORIZONTAL; case DoubleLine: return Symbols.DOUBLE_LINE_HORIZONTAL; default: return ' '; } } private char getVerticalCharacter(TableCellBorderStyle style) { switch(style) { case SingleLine: return Symbols.SINGLE_LINE_VERTICAL; case DoubleLine: return Symbols.DOUBLE_LINE_VERTICAL; default: return ' '; } } private char getJunctionCharacter(TableCellBorderStyle mainStyle, TableCellBorderStyle styleAbove, TableCellBorderStyle styleBelow) { if(mainStyle == TableCellBorderStyle.SingleLine) { if(styleAbove == TableCellBorderStyle.SingleLine) { if(styleBelow == TableCellBorderStyle.SingleLine) { return Symbols.SINGLE_LINE_CROSS; } else if(styleBelow == TableCellBorderStyle.DoubleLine) { //There isn't any character for this, give upper side priority return Symbols.SINGLE_LINE_T_UP; } else { return Symbols.SINGLE_LINE_T_UP; } } else if(styleAbove == TableCellBorderStyle.DoubleLine) { if(styleBelow == TableCellBorderStyle.SingleLine) { //There isn't any character for this, give upper side priority return Symbols.SINGLE_LINE_T_DOUBLE_UP; } else if(styleBelow == TableCellBorderStyle.DoubleLine) { return Symbols.DOUBLE_LINE_VERTICAL_SINGLE_LINE_CROSS; } else { return Symbols.SINGLE_LINE_T_DOUBLE_UP; } } else { if(styleBelow == TableCellBorderStyle.SingleLine) { return Symbols.SINGLE_LINE_T_DOWN; } else if(styleBelow == TableCellBorderStyle.DoubleLine) { return Symbols.SINGLE_LINE_T_DOUBLE_DOWN; } else { return Symbols.SINGLE_LINE_HORIZONTAL; } } } else if(mainStyle == TableCellBorderStyle.DoubleLine) { if(styleAbove == TableCellBorderStyle.SingleLine) { if(styleBelow == TableCellBorderStyle.SingleLine) { return Symbols.DOUBLE_LINE_HORIZONTAL_SINGLE_LINE_CROSS; } else if(styleBelow == TableCellBorderStyle.DoubleLine) { //There isn't any character for this, give upper side priority return Symbols.DOUBLE_LINE_T_SINGLE_UP; } else { return Symbols.DOUBLE_LINE_T_SINGLE_UP; } } else if(styleAbove == TableCellBorderStyle.DoubleLine) { if(styleBelow == TableCellBorderStyle.SingleLine) { //There isn't any character for this, give upper side priority return Symbols.DOUBLE_LINE_T_UP; } else if(styleBelow == TableCellBorderStyle.DoubleLine) { return Symbols.DOUBLE_LINE_CROSS; } else { return Symbols.DOUBLE_LINE_T_UP; } } else { if(styleBelow == TableCellBorderStyle.SingleLine) { return Symbols.DOUBLE_LINE_T_SINGLE_DOWN; } else if(styleBelow == TableCellBorderStyle.DoubleLine) { return Symbols.DOUBLE_LINE_T_DOWN; } else { return Symbols.DOUBLE_LINE_HORIZONTAL; } } } else { return ' '; } } }