X-Git-Url: http://git.nikiroo.be/?p=fanfix.git;a=blobdiff_plain;f=src%2Fbe%2Fnikiroo%2Fjexer%2FTTable.java;fp=src%2Fbe%2Fnikiroo%2Fjexer%2FTTable.java;h=45e5df216024afd7db591d122a190372f16db1fc;hp=0000000000000000000000000000000000000000;hb=8f34a7954f96acdd5d7be5ed6f08fea2713f7d75;hpb=4162793727db52a12e0efeaca48ac5dbdcb57bdf diff --git a/src/be/nikiroo/jexer/TTable.java b/src/be/nikiroo/jexer/TTable.java new file mode 100644 index 0000000..45e5df2 --- /dev/null +++ b/src/be/nikiroo/jexer/TTable.java @@ -0,0 +1,516 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2019 David "Niki" ROULET + * + * 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 David ROULET [niki@nikiroo.be] + * @version 1 + */ +package be.nikiroo.jexer; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import javax.swing.table.TableModel; + +import be.nikiroo.jexer.TTableCellRenderer.CellRendererMode; +import jexer.TAction; +import jexer.TWidget; +import jexer.bits.CellAttributes; + +/** + * A table widget to display and browse through tabular data. + *

+ * Currently, you can only select a line (a row) at a time, but the data you + * present is still tabular. You also access the data in a tabular way (by + * (raw,column)). + * + * @author niki + */ +public class TTable extends TBrowsableWidget { + // Default renderers use text mode + static private TTableCellRenderer defaultSeparatorRenderer = new TTableCellRendererText( + CellRendererMode.SEPARATOR); + static private TTableCellRenderer defaultHeaderRenderer = new TTableCellRendererText( + CellRendererMode.HEADER); + static private TTableCellRenderer defaultHeaderSeparatorRenderer = new TTableCellRendererText( + CellRendererMode.HEADER_SEPARATOR); + + private boolean showHeader; + + private List columns = new ArrayList(); + private TableModel model; + + private int selectedColumn; + + private TTableCellRenderer separatorRenderer; + private TTableCellRenderer headerRenderer; + private TTableCellRenderer headerSeparatorRenderer; + + /** + * The action to perform when the user selects an item (clicks or enter). + */ + private TAction enterAction = null; + + /** + * The action to perform when the user navigates with keyboard. + */ + private TAction moveAction = null; + + /** + * Create a new {@link TTable}. + * + * @param parent + * the parent widget + * @param x + * the X position + * @param y + * the Y position + * @param width + * the width of the {@link TTable} + * @param height + * the height of the {@link TTable} + * @param enterAction + * an action to call when a cell is selected + * @param moveAction + * an action to call when the currently active cell is changed + */ + public TTable(TWidget parent, int x, int y, int width, int height, + final TAction enterAction, final TAction moveAction) { + this(parent, x, y, width, height, enterAction, moveAction, null, false); + } + + /** + * Create a new {@link TTable}. + * + * @param parent + * the parent widget + * @param x + * the X position + * @param y + * the Y position + * @param width + * the width of the {@link TTable} + * @param height + * the height of the {@link TTable} + * @param enterAction + * an action to call when a cell is selected + * @param moveAction + * an action to call when the currently active cell is changed + * @param headers + * the headers of the {@link TTable} + * @param showHeaders + * TRUE to show the headers on screen + */ + public TTable(TWidget parent, int x, int y, int width, int height, + final TAction enterAction, final TAction moveAction, + List headers, boolean showHeaders) { + super(parent, x, y, width, height); + + this.model = new TTableModel(new Object[][] {}); + setSelectedRow(-1); + this.selectedColumn = -1; + + setHeaders(headers, showHeaders); + + this.enterAction = enterAction; + this.moveAction = moveAction; + + reflowData(); + } + + /** + * The data model (containing the actual data) used by this {@link TTable}, + * as with the usual Swing tables. + * + * @return the model + */ + public TableModel getModel() { + return model; + } + + /** + * The data model (containing the actual data) used by this {@link TTable}, + * as with the usual Swing tables. + *

+ * Will reset all the rendering cells. + * + * @param model + * the new model + */ + public void setModel(TableModel model) { + this.model = model; + reflowData(); + } + + /** + * The columns used by this {@link TTable} (you need to access them if you + * want to change the way they are rendered, for instance, or their size). + * + * @return the columns + */ + public List getColumns() { + return columns; + } + + /** + * The {@link TTableCellRenderer} used by the separators (one separator + * between two data columns). + * + * @return the renderer, or the default one if none is set (never NULL) + */ + public TTableCellRenderer getSeparatorRenderer() { + return separatorRenderer != null ? separatorRenderer + : defaultSeparatorRenderer; + } + + /** + * The {@link TTableCellRenderer} used by the separators (one separator + * between two data columns). + * + * @param separatorRenderer + * the new renderer, or NULL to use the default renderer + */ + public void setSeparatorRenderer(TTableCellRenderer separatorRenderer) { + this.separatorRenderer = separatorRenderer; + } + + /** + * The {@link TTableCellRenderer} used by the headers (if + * {@link TTable#isShowHeader()} is enabled, the first line represents the + * headers with the column names). + * + * @return the renderer, or the default one if none is set (never NULL) + */ + public TTableCellRenderer getHeaderRenderer() { + return headerRenderer != null ? headerRenderer : defaultHeaderRenderer; + } + + /** + * The {@link TTableCellRenderer} used by the headers (if + * {@link TTable#isShowHeader()} is enabled, the first line represents the + * headers with the column names). + * + * @param headerRenderer + * the new renderer, or NULL to use the default renderer + */ + public void setHeaderRenderer(TTableCellRenderer headerRenderer) { + this.headerRenderer = headerRenderer; + } + + /** + * The {@link TTableCellRenderer} to use on separators in header lines (see + * the related methods to understand what each of them is). + * + * @return the renderer, or the default one if none is set (never NULL) + */ + public TTableCellRenderer getHeaderSeparatorRenderer() { + return headerSeparatorRenderer != null ? headerSeparatorRenderer + : defaultHeaderSeparatorRenderer; + } + + /** + * The {@link TTableCellRenderer} to use on separators in header lines (see + * the related methods to understand what each of them is). + * + * @param headerSeparatorRenderer + * the new renderer, or NULL to use the default renderer + */ + public void setHeaderSeparatorRenderer( + TTableCellRenderer headerSeparatorRenderer) { + this.headerSeparatorRenderer = headerSeparatorRenderer; + } + + /** + * Show the header row on this {@link TTable}. + * + * @return TRUE if we show them + */ + public boolean isShowHeader() { + return showHeader; + } + + /** + * Show the header row on this {@link TTable}. + * + * @param showHeader + * TRUE to show them + */ + public void setShowHeader(boolean showHeader) { + this.showHeader = showHeader; + setYOffset(showHeader ? 2 : 0); + reflowData(); + } + + /** + * Change the headers of the table. + *

+ * Note that this method is a convenience method that will create columns of + * the corresponding names and set them. As such, the previous columns if + * any will be replaced. + * + * @param headers + * the new headers + */ + public void setHeaders(List headers) { + setHeaders(headers, showHeader); + } + + /** + * Change the headers of the table. + *

+ * Note that this method is a convenience method that will create columns of + * the corresponding names and set them in the same order. As such, the + * previous columns if any will be replaced. + * + * @param headers + * the new headers + * @param showHeader + * TRUE to show them on screen + */ + public void setHeaders(List headers, boolean showHeader) { + if (headers == null) { + headers = new ArrayList(); + } + + int i = 0; + this.columns = new ArrayList(); + for (Object header : headers) { + this.columns.add(new TTableColumn(i++, header, getModel())); + } + + setShowHeader(showHeader); + } + + /** + * Set the data and create a new {@link TTableModel} for them. + * + * @param data + * the data to set into this table, as an array of rows, that is, + * an array of arrays of values + */ + + public void setRowData(Object[][] data) { + setRowData(TTableModel.convert(data)); + } + + /** + * Set the data and create a new {@link TTableModel} for them. + * + * @param data + * the data to set into this table, as a collection of rows, that + * is, a collection of collections of values + */ + public void setRowData( + final Collection> data) { + setModel(new TTableModel(data)); + } + + /** + * The currently selected cell. + * + * @return the cell + */ + public Object getSelectedCell() { + int selectedRow = getSelectedRow(); + if (selectedRow >= 0 && selectedColumn >= 0) { + return model.getValueAt(selectedRow, selectedColumn); + } + + return null; + } + + @Override + public int getRowCount() { + if (model == null) { + return 0; + } + return model.getRowCount(); + } + + @Override + public int getColumnCount() { + if (model == null) { + return 0; + } + return model.getColumnCount(); + } + + @Override + public void dispatchEnter(int selectedRow) { + super.dispatchEnter(selectedRow); + if (enterAction != null) { + enterAction.DO(); + } + } + + @Override + public void dispatchMove(int fromRow, int toRow) { + super.dispatchMove(fromRow, toRow); + if (moveAction != null) { + moveAction.DO(); + } + } + + /** + * Clear the content of the {@link TTable}. + *

+ * It will not affect the headers. + *

+ * You may want to call {@link TTable#reflowData()} when done to see the + * changes. + */ + public void clear() { + setSelectedRow(-1); + selectedColumn = -1; + setModel(new TTableModel(new Object[][] {})); + } + + @Override + public void reflowData() { + super.reflowData(); + + int lastAutoColumn = -1; + int rowWidth = 0; + + int i = 0; + for (TTableColumn tcol : columns) { + tcol.reflowData(); + + if (!tcol.isForcedWidth()) { + lastAutoColumn = i; + } + + rowWidth += tcol.getWidth(); + + i++; + } + + if (!columns.isEmpty()) { + rowWidth += (i - 1) * getSeparatorRenderer().getWidthOf(null); + + int extraWidth = getWidth() - rowWidth; + if (extraWidth > 0) { + if (lastAutoColumn < 0) { + lastAutoColumn = columns.size() - 1; + } + TTableColumn tcol = columns.get(lastAutoColumn); + tcol.expandWidthTo(tcol.getWidth() + extraWidth); + rowWidth += extraWidth; + } + } + } + + @Override + public void draw() { + int begin = vScroller.getValue(); + int y = this.showHeader ? 2 : 0; + + if (showHeader) { + CellAttributes colorHeaders = getHeaderRenderer() + .getCellAttributes(getTheme(), false, isAbsoluteActive()); + drawRow(-1, 0); + String formatString = "%-" + Integer.toString(getWidth()) + "s"; + String data = String.format(formatString, ""); + getScreen().putStringXY(0, 1, data, colorHeaders); + } + + // draw the actual rows until no more, + // then pad the rest with blank rows + for (int i = begin; i < getRowCount(); i++) { + drawRow(i, y); + y++; + + // -2: window borders + if (y >= getHeight() - 2 - getHorizontalScroller().getHeight()) { + break; + } + } + + CellAttributes emptyRowColor = getSeparatorRenderer() + .getCellAttributes(getTheme(), false, isAbsoluteActive()); + for (int i = getRowCount(); i < getHeight(); i++) { + getScreen().hLineXY(0, y, getWidth() - 1, ' ', emptyRowColor); + y++; + } + } + + @Override + protected int getVirtualWidth() { + int width = 0; + + if (getColumns() != null) { + for (TTableColumn tcol : getColumns()) { + width += tcol.getWidth(); + } + + if (getColumnCount() > 0) { + width += (getColumnCount() - 1) + * getSeparatorRenderer().getWidthOf(null); + } + } + + return width; + } + + @Override + protected int getVirtualHeight() { + // TODO: allow changing the height of one row + return (showHeader ? 2 : 0) + (getRowCount() * 1); + } + + /** + * Draw the given row (it MUST exist) at the specified index and + * offset. + * + * @param rowIndex + * the index of the row to draw or -1 for the headers + * @param y + * the Y position + */ + private void drawRow(int rowIndex, int y) { + for (int i = 0; i < getColumnCount(); i++) { + TTableColumn tcol = columns.get(i); + Object value; + if (rowIndex < 0) { + value = tcol.getHeaderValue(); + } else { + value = model.getValueAt(rowIndex, tcol.getModelIndex()); + } + + if (i > 0) { + TTableCellRenderer sep = rowIndex < 0 ? getHeaderSeparatorRenderer() + : getSeparatorRenderer(); + sep.renderTableCell(this, null, rowIndex, i - 1, y); + } + + if (rowIndex < 0) { + getHeaderRenderer() + .renderTableCell(this, value, rowIndex, i, y); + } else { + tcol.getRenderer().renderTableCell(this, value, rowIndex, i, y); + } + } + } +}