From: Niki Roo Date: Tue, 12 Mar 2019 17:49:52 +0000 (+0100) Subject: update jexer X-Git-Tag: fanfix-2.0.0~64 X-Git-Url: https://git.nikiroo.be/?a=commitdiff_plain;h=8f34a7954f96acdd5d7be5ed6f08fea2713f7d75;p=fanfix.git update jexer --- diff --git a/libs/jexer-0.0.6-dev-sources.jar b/libs/jexer-0.0.6-dev-sources.jar deleted file mode 100644 index 5fb7f2d..0000000 Binary files a/libs/jexer-0.0.6-dev-sources.jar and /dev/null differ diff --git a/libs/jexer-0.3.1-Gitlab_2019-03-09-niki1-sources.jar b/libs/jexer-0.3.1-Gitlab_2019-03-09-niki1-sources.jar new file mode 100644 index 0000000..0e9c0c3 Binary files /dev/null and b/libs/jexer-0.3.1-Gitlab_2019-03-09-niki1-sources.jar differ diff --git a/src/be/nikiroo/fanfix/reader/tui/TuiReaderMainWindow.java b/src/be/nikiroo/fanfix/reader/tui/TuiReaderMainWindow.java index c103171..76c05d0 100644 --- a/src/be/nikiroo/fanfix/reader/tui/TuiReaderMainWindow.java +++ b/src/be/nikiroo/fanfix/reader/tui/TuiReaderMainWindow.java @@ -189,7 +189,6 @@ class TuiReaderMainWindow extends TWindow { }; selectBox = addComboBox(0, 0, 10, selects, 0, -1, onSelect); - selectBox.setReadOnly(true); selectTargetBox = addComboBox(0, 0, 0, selectTargets, 0, -1, new TAction() { @@ -203,7 +202,6 @@ class TuiReaderMainWindow extends TWindow { } } }); - selectTargetBox.setReadOnly(true); // Set defaults onSelect.DO(); diff --git a/src/be/nikiroo/fanfix/reader/tui/TuiReaderStoryWindow.java b/src/be/nikiroo/fanfix/reader/tui/TuiReaderStoryWindow.java index d4fac73..dc0afc2 100644 --- a/src/be/nikiroo/fanfix/reader/tui/TuiReaderStoryWindow.java +++ b/src/be/nikiroo/fanfix/reader/tui/TuiReaderStoryWindow.java @@ -10,7 +10,6 @@ import java.util.List; import jexer.TAction; import jexer.TButton; import jexer.TLabel; -import jexer.TTable; import jexer.TText; import jexer.TWindow; import jexer.event.TCommandEvent; @@ -20,6 +19,7 @@ import be.nikiroo.fanfix.data.MetaData; import be.nikiroo.fanfix.data.Paragraph; import be.nikiroo.fanfix.data.Paragraph.ParagraphType; import be.nikiroo.fanfix.data.Story; +import be.nikiroo.jexer.TTable; import be.nikiroo.utils.StringUtils; /** @@ -50,8 +50,7 @@ class TuiReaderStoryWindow extends TWindow { // last = use window background titleField = new TLabel(this, " Title", 0, 1, "tlabel", false); textField = new TText(this, "", 0, 0, 1, 1); - table = new TTable(this, 0, 0, 1, 1, null, null, Arrays.asList("Key", - "Value"), true); + table = new TTable(this, 0, 0, 1, 1, null, null, Arrays.asList("Key", "Value"), true); titleField.setEnabled(false); diff --git a/src/be/nikiroo/jexer/TBrowsableWidget.java b/src/be/nikiroo/jexer/TBrowsableWidget.java new file mode 100644 index 0000000..aa18d09 --- /dev/null +++ b/src/be/nikiroo/jexer/TBrowsableWidget.java @@ -0,0 +1,404 @@ +/* + * 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 static jexer.TKeypress.kbBackTab; +import static jexer.TKeypress.kbDown; +import static jexer.TKeypress.kbEnd; +import static jexer.TKeypress.kbEnter; +import static jexer.TKeypress.kbHome; +import static jexer.TKeypress.kbLeft; +import static jexer.TKeypress.kbPgDn; +import static jexer.TKeypress.kbPgUp; +import static jexer.TKeypress.kbRight; +import static jexer.TKeypress.kbShiftTab; +import static jexer.TKeypress.kbTab; +import static jexer.TKeypress.kbUp; +import jexer.THScroller; +import jexer.TScrollableWidget; +import jexer.TVScroller; +import jexer.TWidget; +import jexer.event.TKeypressEvent; +import jexer.event.TMouseEvent; +import jexer.event.TResizeEvent; + +/** + * This class represents a browsable {@link TWidget}, that is, a {@link TWidget} + * where you can use the keyboard or mouse to browse to one line to the next, or + * from left t right. + * + * @author niki + */ +abstract public class TBrowsableWidget extends TScrollableWidget { + private int selectedRow; + private int selectedColumn; + private int yOffset; + + /** + * The number of rows in this {@link TWidget}. + * + * @return the number of rows + */ + abstract protected int getRowCount(); + + /** + * The number of columns in this {@link TWidget}. + * + * @return the number of columns + */ + abstract protected int getColumnCount(); + + /** + * The virtual width of this {@link TWidget}, that is, the total width it + * can take to display all the data. + * + * @return the width + */ + abstract int getVirtualWidth(); + + /** + * The virtual height of this {@link TWidget}, that is, the total width it + * can take to display all the data. + * + * @return the height + */ + abstract int getVirtualHeight(); + + /** + * Basic setup of this class (called by all constructors) + */ + private void setup() { + vScroller = new TVScroller(this, 0, 0, 1); + hScroller = new THScroller(this, 0, 0, 1); + fixScrollers(); + } + + /** + * Create a new {@link TBrowsableWidget} linked to the given {@link TWidget} + * parent. + * + * @param parent + * parent widget + */ + protected TBrowsableWidget(final TWidget parent) { + super(parent); + setup(); + } + + /** + * Create a new {@link TBrowsableWidget} linked to the given {@link TWidget} + * parent. + * + * @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 + */ + protected TBrowsableWidget(final TWidget parent, final int x, final int y, + final int width, final int height) { + super(parent, x, y, width, height); + setup(); + } + + /** + * Create a new {@link TBrowsableWidget} linked to the given {@link TWidget} + * parent. + * + * @param parent + * parent widget + * @param enabled + * if true assume enabled + */ + protected TBrowsableWidget(final TWidget parent, final boolean enabled) { + super(parent, enabled); + setup(); + } + + /** + * Create a new {@link TBrowsableWidget} linked to the given {@link TWidget} + * parent. + * + * @param parent + * parent widget + * @param enabled + * if true assume enabled + * @param x + * column relative to parent + * @param y + * row relative to parent + * @param width + * width of widget + * @param height + * height of widget + */ + protected TBrowsableWidget(final TWidget parent, final boolean enabled, + final int x, final int y, final int width, final int height) { + super(parent, enabled, x, y, width, height); + setup(); + } + + /** + * The currently selected row (or -1 if no row is selected). + * + * @return the selected row + */ + public int getSelectedRow() { + return selectedRow; + } + + /** + * The currently selected row (or -1 if no row is selected). + *

+ * You may want to call {@link TBrowsableWidget#reflowData()} when done to + * see the changes. + * + * @param selectedRow + * the new selected row + * + * @throws IndexOutOfBoundsException + * when the index is out of bounds + */ + public void setSelectedRow(int selectedRow) { + if (selectedRow < -1 || selectedRow >= getRowCount()) { + throw new IndexOutOfBoundsException(String.format( + "Cannot set row %d on a table with %d rows", selectedRow, + getRowCount())); + } + + this.selectedRow = selectedRow; + } + + /** + * The currently selected column (or -1 if no column is selected). + * + * @return the new selected column + */ + public int getSelectedColumn() { + return selectedColumn; + } + + /** + * The currently selected column (or -1 if no column is selected). + *

+ * You may want to call {@link TBrowsableWidget#reflowData()} when done to + * see the changes. + * + * @param selectedColumn + * the new selected column + * + * @throws IndexOutOfBoundsException + * when the index is out of bounds + */ + public void setSelectedColumn(int selectedColumn) { + if (selectedColumn < -1 || selectedColumn >= getColumnCount()) { + throw new IndexOutOfBoundsException(String.format( + "Cannot set column %d on a table with %d columns", + selectedColumn, getColumnCount())); + } + + this.selectedColumn = selectedColumn; + } + + /** + * An offset on the Y position of the table, i.e., the number of rows to + * skip so the control can draw that many rows always on top. + * + * @return the offset + */ + public int getYOffset() { + return yOffset; + } + + /** + * An offset on the Y position of the table, i.e., the number of rows that + * should always stay on top. + * + * @param yOffset + * the new offset + */ + public void setYOffset(int yOffset) { + this.yOffset = yOffset; + } + + @SuppressWarnings("unused") + public void dispatchMove(int fromRow, int toRow) { + reflowData(); + } + + @SuppressWarnings("unused") + public void dispatchEnter(int selectedRow) { + reflowData(); + } + + @Override + public void onMouseDown(final TMouseEvent mouse) { + if (mouse.isMouseWheelUp()) { + vScroller.decrement(); + return; + } + if (mouse.isMouseWheelDown()) { + vScroller.increment(); + return; + } + + if ((mouse.getX() < getWidth() - 1) && (mouse.getY() < getHeight() - 1)) { + if (vScroller.getValue() + mouse.getY() < getRowCount()) { + selectedRow = vScroller.getValue() + mouse.getY() + - getYOffset(); + } + dispatchEnter(selectedRow); + return; + } + + // Pass to children + super.onMouseDown(mouse); + } + + @Override + public void onKeypress(final TKeypressEvent keypress) { + int maxX = getRowCount(); + int prevSelectedRow = selectedRow; + + int firstLineIndex = vScroller.getValue() - getYOffset() + 2; + int lastLineIndex = firstLineIndex - hScroller.getHeight() + + getHeight() - 2 - 2; + + if (keypress.equals(kbLeft)) { + hScroller.decrement(); + } else if (keypress.equals(kbRight)) { + hScroller.increment(); + } else if (keypress.equals(kbUp)) { + if (maxX > 0 && selectedRow < maxX) { + if (selectedRow > 0) { + if (selectedRow <= firstLineIndex) { + vScroller.decrement(); + } + selectedRow--; + } else { + selectedRow = 0; + } + + dispatchMove(prevSelectedRow, selectedRow); + } + } else if (keypress.equals(kbDown)) { + if (maxX > 0) { + if (selectedRow >= 0) { + if (selectedRow < maxX - 1) { + selectedRow++; + if (selectedRow >= lastLineIndex) { + vScroller.increment(); + } + } + } else { + selectedRow = 0; + } + + dispatchMove(prevSelectedRow, selectedRow); + } + } else if (keypress.equals(kbPgUp)) { + if (selectedRow >= 0) { + vScroller.bigDecrement(); + selectedRow -= getHeight() - 1; + if (selectedRow < 0) { + selectedRow = 0; + } + + dispatchMove(prevSelectedRow, selectedRow); + } + } else if (keypress.equals(kbPgDn)) { + if (selectedRow >= 0) { + vScroller.bigIncrement(); + selectedRow += getHeight() - 1; + if (selectedRow > getRowCount() - 1) { + selectedRow = getRowCount() - 1; + } + + dispatchMove(prevSelectedRow, selectedRow); + } + } else if (keypress.equals(kbHome)) { + if (getRowCount() > 0) { + vScroller.toTop(); + selectedRow = 0; + dispatchMove(prevSelectedRow, selectedRow); + } + } else if (keypress.equals(kbEnd)) { + if (getRowCount() > 0) { + vScroller.toBottom(); + selectedRow = getRowCount() - 1; + dispatchMove(prevSelectedRow, selectedRow); + } + } else if (keypress.equals(kbTab)) { + getParent().switchWidget(true); + } else if (keypress.equals(kbShiftTab) || keypress.equals(kbBackTab)) { + getParent().switchWidget(false); + } else if (keypress.equals(kbEnter)) { + if (selectedRow >= 0) { + dispatchEnter(selectedRow); + } + } else { + // Pass other keys (tab etc.) on + super.onKeypress(keypress); + } + } + + @Override + public void onResize(TResizeEvent event) { + super.onResize(event); + reflowData(); + } + + @Override + public void reflowData() { + super.reflowData(); + fixScrollers(); + } + + private void fixScrollers() { + vScroller.setX(Math.max(0, getWidth() - 3)); + vScroller.setHeight(Math.max(1, getHeight() - 2)); + hScroller.setY(Math.max(0, getHeight() - 3)); + hScroller.setWidth(Math.max(1, getWidth() - 3)); + + // virtual_size + // - the other scroll bar size + // + 2 (for the border of the window) + vScroller.setTopValue(0); + vScroller.setBottomValue(Math.max(0, getVirtualHeight() - getHeight() + + hScroller.getHeight() + 2)); + hScroller.setLeftValue(0); + hScroller.setRightValue(Math.max(0, getVirtualWidth() - getWidth() + + vScroller.getWidth() + 2)); + } +} 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); + } + } + } +} diff --git a/src/be/nikiroo/jexer/TTableCellRenderer.java b/src/be/nikiroo/jexer/TTableCellRenderer.java new file mode 100644 index 0000000..6d7b3b3 --- /dev/null +++ b/src/be/nikiroo/jexer/TTableCellRenderer.java @@ -0,0 +1,240 @@ +/* + * 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 jexer.bits.CellAttributes; +import jexer.bits.ColorTheme; + +/** + * A {@link TTable} cell renderer allows you to customize the way a single cell + * will be displayed on screen. + *

+ * It can be used in a {@link TTable} for the haeders or the separators or in a + * {@link TTableColumn} for the data. + * + * @author niki + */ +abstract public class TTableCellRenderer { + private CellRendererMode mode; + + /** + * The simple renderer mode. + * + * @author niki + */ + public enum CellRendererMode { + /** Normal text mode */ + NORMAL, + /** Only display a separator */ + SEPARATOR, + /** Header text mode */ + HEADER, + /** Both HEADER and SEPARATOR at once */ + HEADER_SEPARATOR; + + /** + * This mode represents a separator. + * + * @return TRUE for separators + */ + public boolean isSeparator() { + return this == SEPARATOR || this == HEADER_SEPARATOR; + } + + /** + * This mode represents a header. + * + * @return TRUE for headers + */ + public boolean isHeader() { + return this == HEADER || this == HEADER_SEPARATOR; + } + } + + /** + * Create a new renderer of the given mode. + * + * @param mode + * the renderer mode, cannot be NULL + */ + public TTableCellRenderer(CellRendererMode mode) { + if (mode == null) { + throw new IllegalArgumentException( + "Cannot create a renderer of type NULL"); + } + + this.mode = mode; + } + + /** + * Render the given value. + * + * @param table + * the table to write on + * @param value + * the value to write + * @param rowIndex + * the row index in the table + * @param colIndex + * the column index in the table + * @param y + * the Y position at which to draw this row + */ + abstract public void renderTableCell(TTable table, Object value, + int rowIndex, int colIndex, int y); + + /** + * The mode of this {@link TTableCellRenderer}. + * + * @return the mode + */ + public CellRendererMode getMode() { + return mode; + } + + /** + * The cell attributes to use for the given state. + * + * @param theme + * the color theme to use + * @param isSelected + * TRUE if the cell is selected + * @param hasFocus + * TRUE if the cell has focus + * + * @return the attributes + */ + public CellAttributes getCellAttributes(ColorTheme theme, + boolean isSelected, boolean hasFocus) { + return theme.getColor(getColorKey(isSelected, hasFocus)); + } + + /** + * Measure the width of the value. + * + * @param value + * the value to measure + * + * @return its width + */ + public int getWidthOf(Object value) { + if (getMode().isSeparator()) { + return asText(null, 0, false).length(); + } + return ("" + value).length(); + } + + /** + * The colour to use for the given state, specified as a Jexer colour key. + * + * @param isSelected + * TRUE if the cell is selected + * @param hasFocus + * TRUE if the cell has focus + * + * @return the colour key + */ + protected String getColorKey(boolean isSelected, boolean hasFocus) { + if (mode.isHeader()) { + return "tlabel"; + } + + String colorKey = "tlist"; + if (isSelected) { + colorKey += ".selected"; + } else if (!hasFocus) { + colorKey += ".inactive"; + } + + return colorKey; + } + + /** + * Return the X offset to use to draw a column at the given index. + * + * @param table + * the table to draw into + * @param colIndex + * the column index + * + * @return the offset + */ + protected int getXOffset(TTable table, int colIndex) { + int xOffset = -table.getHorizontalValue(); + for (int i = 0; i <= colIndex; i++) { + TTableColumn tcol = table.getColumns().get(i); + xOffset += tcol.getWidth(); + if (i > 0) { + xOffset += table.getSeparatorRenderer().getWidthOf(null); + } + } + + TTableColumn tcol = table.getColumns().get(colIndex); + if (!getMode().isSeparator()) { + xOffset -= tcol.getWidth(); + } + + return xOffset; + } + + /** + * Return the text to use (usually the converted-to-text value, except for + * the special separator mode). + * + * @param value + * the value to get the text of + * @param width + * the width we should tale + * @param align + * the text to the right + * + * @return the {@link String} to display + */ + protected String asText(Object value, int width, boolean rightAlign) { + if (getMode().isSeparator()) { + // some nice characters for the separator: ┃ │ | + return " │ "; + } + + if (width <= 0) { + return ""; + } + + String format; + if (!rightAlign) { + // Left align + format = "%-" + width + "s"; + } else { + // right align + format = "%" + width + "s"; + } + + return String.format(format, value); + } +} \ No newline at end of file diff --git a/src/be/nikiroo/jexer/TTableCellRendererText.java b/src/be/nikiroo/jexer/TTableCellRendererText.java new file mode 100644 index 0000000..8f81883 --- /dev/null +++ b/src/be/nikiroo/jexer/TTableCellRendererText.java @@ -0,0 +1,91 @@ +/* + * 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 jexer.bits.CellAttributes; + +/** + * A simple {@link TTableCellRenderer} that display the values within a + * {@link TLabel}. + *

+ * It supports a few different modes, see + * {@link TTableOldSimpleTextCellRenderer.CellRendererMode}. + * + * @author niki + */ +public class TTableCellRendererText extends TTableCellRenderer { + private boolean rightAlign; + + /** + * Create a new renderer for normal text mode. + */ + public TTableCellRendererText() { + this(CellRendererMode.NORMAL); + } + + /** + * Create a new renderer of the given mode. + * + * @param mode + * the renderer mode + */ + public TTableCellRendererText(CellRendererMode mode) { + this(mode, false); + } + + /** + * Create a new renderer of the given mode. + * + * @param mode + * the renderer mode, cannot be NULL + */ + public TTableCellRendererText(CellRendererMode mode, + boolean rightAlign) { + super(mode); + + this.rightAlign = rightAlign; + } + + @Override + public void renderTableCell(TTable table, Object value, int rowIndex, + int colIndex, int y) { + + int xOffset = getXOffset(table, colIndex); + TTableColumn tcol = table.getColumns().get(colIndex); + String data = asText(value, tcol.getWidth(), rightAlign); + + if (!data.isEmpty()) { + boolean isSelected = table.getSelectedRow() == rowIndex; + boolean hasFocus = table.isAbsoluteActive(); + CellAttributes color = getCellAttributes(table.getWindow() + .getApplication().getTheme(), isSelected, hasFocus); + table.getScreen().putStringXY(xOffset, y, data, color); + } + } +} diff --git a/src/be/nikiroo/jexer/TTableCellRendererWidget.java b/src/be/nikiroo/jexer/TTableCellRendererWidget.java new file mode 100644 index 0000000..22c6f47 --- /dev/null +++ b/src/be/nikiroo/jexer/TTableCellRendererWidget.java @@ -0,0 +1,170 @@ +/* + * 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.HashMap; +import java.util.Map; + +import jexer.TLabel; +import jexer.TWidget; + +/** + * A simple {@link TTableCellRenderer} that display the values within a + * {@link TLabel}. + *

+ * It supports a few different modes, see + * {@link TTableSimpleTextCellRenderer.CellRendererMode}. + * + * @author niki + */ +public class TTableCellRendererWidget extends TTableCellRenderer { + private boolean rightAlign; + private Map widgets = new HashMap(); + + /** + * Create a new renderer for normal text mode. + */ + public TTableCellRendererWidget() { + this(CellRendererMode.NORMAL); + } + + /** + * Create a new renderer of the given mode. + * + * @param mode + * the renderer mode + */ + public TTableCellRendererWidget(CellRendererMode mode) { + this(mode, false); + } + + /** + * Create a new renderer of the given mode. + * + * @param mode + * the renderer mode, cannot be NULL + */ + public TTableCellRendererWidget(CellRendererMode mode, boolean rightAlign) { + super(mode); + + this.rightAlign = rightAlign; + } + + @Override + public void renderTableCell(TTable table, Object value, int rowIndex, + int colIndex, int y) { + + String wkey = "[Row " + y + " " + getMode() + "]"; + TWidget widget = widgets.get(wkey); + + TTableColumn tcol = table.getColumns().get(colIndex); + boolean isSelected = table.getSelectedRow() == rowIndex; + boolean hasFocus = table.isAbsoluteActive(); + int width = tcol.getWidth(); + + int xOffset = getXOffset(table, colIndex); + + if (widget != null + && !updateTableCellRendererComponent(widget, value, isSelected, + hasFocus, y, xOffset, width)) { + table.removeChild(widget); + widget = null; + } + + if (widget == null) { + widget = getTableCellRendererComponent(table, value, isSelected, + hasFocus, y, xOffset, width); + } + + widgets.put(wkey, widget); + } + + /** + * Create a new {@link TWidget} to represent the given value. + * + * @param table + * the parent {@link TTable} + * @param value + * the value to represent + * @param isSelected + * TRUE if selected + * @param hasFocus + * TRUE if focused + * @param row + * the row to draw it at + * @param column + * the column to draw it at + * @param width + * the width of the control + * + * @return the widget + */ + protected TWidget getTableCellRendererComponent(TTable table, Object value, + boolean isSelected, boolean hasFocus, int row, int column, int width) { + return new TLabel(table, asText(value, width, rightAlign), column, row, + getColorKey(isSelected, hasFocus), false); + } + + /** + * Update the content of the widget if at all possible. + * + * @param component + * the component to update + * @param value + * the value to represent + * @param isSelected + * TRUE if selected + * @param hasFocus + * TRUE if focused + * @param row + * the row to draw it at + * @param column + * the column to draw it at + * @param width + * the width of the control + * + * @return TRUE if the operation was possible, FALSE if it failed + */ + protected boolean updateTableCellRendererComponent(TWidget component, + Object value, boolean isSelected, boolean hasFocus, int row, + int column, int width) { + + if (component instanceof TLabel) { + TLabel widget = (TLabel) component; + widget.setLabel(asText(value, width, rightAlign)); + widget.setColorKey(getColorKey(isSelected, hasFocus)); + widget.setWidth(width); + widget.setX(column); + widget.setY(row); + return true; + } + + return false; + } +} diff --git a/src/be/nikiroo/jexer/TTableColumn.java b/src/be/nikiroo/jexer/TTableColumn.java new file mode 100644 index 0000000..3eea230 --- /dev/null +++ b/src/be/nikiroo/jexer/TTableColumn.java @@ -0,0 +1,129 @@ +package be.nikiroo.jexer; + +import javax.swing.table.TableModel; + +import be.nikiroo.jexer.TTableCellRenderer.CellRendererMode; + +public class TTableColumn { + static private TTableCellRenderer defaultrenderer = new TTableCellRendererText( + CellRendererMode.NORMAL); + + private TableModel model; + private int modelIndex; + private int width; + private boolean forcedWidth; + + private TTableCellRenderer renderer; + + /** The auto-computed width of the column (the width of the largest value) */ + private int autoWidth; + + private Object headerValue; + + public TTableColumn(int modelIndex) { + this(modelIndex, null); + } + + public TTableColumn(int modelIndex, String colName) { + this(modelIndex, colName, null); + } + + // set the width and preferred with the the max data size + public TTableColumn(int modelIndex, Object colValue, TableModel model) { + this.model = model; + this.modelIndex = modelIndex; + + reflowData(); + + if (colValue != null) { + setHeaderValue(colValue); + } + } + + // never null + public TTableCellRenderer getRenderer() { + return renderer != null ? renderer : defaultrenderer; + } + + public void setCellRenderer(TTableCellRenderer renderer) { + this.renderer = renderer; + } + + /** + * Recompute whatever data is displayed by this widget. + *

+ * Will just update the sizes in this case. + */ + public void reflowData() { + if (model != null) { + int maxDataSize = 0; + for (int i = 0; i < model.getRowCount(); i++) { + maxDataSize = Math.max( + maxDataSize, + getRenderer().getWidthOf( + model.getValueAt(i, modelIndex))); + } + + autoWidth = maxDataSize; + if (!forcedWidth) { + setWidth(maxDataSize); + } + } else { + autoWidth = 0; + forcedWidth = false; + width = 0; + } + } + + public int getModelIndex() { + return modelIndex; + } + + /** + * The actual size of the column. This can be auto-computed in some cases. + * + * @return the width (never < 0) + */ + public int getWidth() { + return width; + } + + /** + * Set the actual size of the column or -1 for auto size. + * + * @param width + * the width (or -1 for auto) + */ + public void setWidth(int width) { + forcedWidth = width >= 0; + + if (forcedWidth) { + this.width = width; + } else { + this.width = autoWidth; + } + } + + /** + * The width was forced by the user (using + * {@link TTableColumn#setWidth(int)} with a positive value). + * + * @return TRUE if it was + */ + public boolean isForcedWidth() { + return forcedWidth; + } + + // not an actual forced width, but does change the width return + void expandWidthTo(int width) { + this.width = width; + } + + public Object getHeaderValue() { + return headerValue; + } + + public void setHeaderValue(Object headerValue) { + this.headerValue = headerValue; + } +} diff --git a/src/be/nikiroo/jexer/TTableLine.java b/src/be/nikiroo/jexer/TTableLine.java new file mode 100644 index 0000000..f393621 --- /dev/null +++ b/src/be/nikiroo/jexer/TTableLine.java @@ -0,0 +1,135 @@ +package be.nikiroo.jexer; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +public class TTableLine implements List { + //TODO: in TTable: default to header of size 1 + private List list; + + public TTableLine(List list) { + this.list = list; + } + + // TODO: override this and the rest shall follow + protected List getList() { + return list; + } + + @Override + public int size() { + return getList().size(); + } + + @Override + public boolean isEmpty() { + return getList().isEmpty(); + } + + @Override + public boolean contains(Object o) { + return getList().contains(o); + } + + @Override + public Iterator iterator() { + return getList().iterator(); + } + + @Override + public Object[] toArray() { + return getList().toArray(); + } + + @Override + public T[] toArray(T[] a) { + return getList().toArray(a); + } + + @Override + public boolean containsAll(Collection c) { + return getList().containsAll(c); + } + + @Override + public String get(int index) { + return getList().get(index); + } + + @Override + public int indexOf(Object o) { + return getList().indexOf(o); + } + + @Override + public int lastIndexOf(Object o) { + return getList().lastIndexOf(o); + } + + @Override + public List subList(int fromIndex, int toIndex) { + return getList().subList(fromIndex, toIndex); + } + + @Override + public ListIterator listIterator() { + return getList().listIterator(); + } + + @Override + public ListIterator listIterator(int index) { + return getList().listIterator(index); + } + + @Override + public boolean add(String e) { + throw new UnsupportedOperationException("Read-only collection"); + } + + @Override + public boolean remove(Object o) { + throw new UnsupportedOperationException("Read-only collection"); + } + + @Override + public boolean addAll(Collection c) { + throw new UnsupportedOperationException("Read-only collection"); + } + + @Override + public boolean addAll(int index, Collection c) { + throw new UnsupportedOperationException("Read-only collection"); + } + + @Override + public boolean removeAll(Collection c) { + throw new UnsupportedOperationException("Read-only collection"); + } + + @Override + public boolean retainAll(Collection c) { + throw new UnsupportedOperationException("Read-only collection"); + } + + @Override + public void clear() { + throw new UnsupportedOperationException("Read-only collection"); + } + + @Override + public String set(int index, String element) { + throw new UnsupportedOperationException("Read-only collection"); + } + + @Override + public void add(int index, String element) { + throw new UnsupportedOperationException("Read-only collection"); + } + + @Override + public String remove(int index) { + throw new UnsupportedOperationException("Read-only collection"); + } +} diff --git a/src/be/nikiroo/jexer/TTableModel.java b/src/be/nikiroo/jexer/TTableModel.java new file mode 100644 index 0000000..cd86d35 --- /dev/null +++ b/src/be/nikiroo/jexer/TTableModel.java @@ -0,0 +1,176 @@ +/* + * 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.Arrays; +import java.util.Collection; + +import javax.swing.event.TableModelListener; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.TableModel; + +/** + * The model of a {@link TTable}. It contains the data of the table and allows + * you access to it. + *

+ * Note that you don't need to send it the representation of the data, but the + * data itself; {@link TTableCellRenderer} is the class responsible of + * representing that data (you can change the headers renderer on a + * {@link TTable} and the cells renderer on each of its {@link TTableColumn}). + *

+ * It works in a similar way to the Java Swing version of it. + * + * @author niki + */ +public class TTableModel implements TableModel { + private TableModel model; + + /** + * Create a new {@link TTableModel} with the given data inside. + * + * @param data + * the data + */ + public TTableModel(Object[][] data) { + this(convert(data)); + } + + /** + * Create a new {@link TTableModel} with the given data inside. + * + * @param data + * the data + */ + public TTableModel( + final Collection> data) { + + int maxItemsPerRow = 0; + for (Collection rowOfData : data) { + maxItemsPerRow = Math.max(maxItemsPerRow, rowOfData.size()); + } + + int i = 0; + final Object[][] odata = new Object[data.size()][maxItemsPerRow]; + for (Collection rowOfData : data) { + odata[i] = new String[maxItemsPerRow]; + int j = 0; + for (Object pieceOfData : rowOfData) { + odata[i][j] = pieceOfData; + j++; + } + i++; + } + + final int maxItemsPerRowFinal = maxItemsPerRow; + this.model = new AbstractTableModel() { + private static final long serialVersionUID = 1L; + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + return odata[rowIndex][columnIndex]; + } + + @Override + public int getRowCount() { + return odata.length; + } + + @Override + public int getColumnCount() { + return maxItemsPerRowFinal; + } + }; + } + + @Override + public int getRowCount() { + return model.getRowCount(); + } + + @Override + public int getColumnCount() { + return model.getColumnCount(); + } + + @Override + public String getColumnName(int columnIndex) { + return model.getColumnName(columnIndex); + } + + @Override + public Class getColumnClass(int columnIndex) { + return model.getColumnClass(columnIndex); + } + + @Override + public boolean isCellEditable(int rowIndex, int columnIndex) { + return model.isCellEditable(rowIndex, columnIndex); + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + return model.getValueAt(rowIndex, columnIndex); + } + + @Override + public void setValueAt(Object aValue, int rowIndex, int columnIndex) { + model.setValueAt(aValue, rowIndex, columnIndex); + } + + @Override + public void addTableModelListener(TableModelListener l) { + model.addTableModelListener(l); + } + + @Override + public void removeTableModelListener(TableModelListener l) { + model.removeTableModelListener(l); + } + + /** + * Helper method to convert an array to a collection. + * + * @param + * + * @param data + * the data + * + * @return the data in another format + */ + static Collection> convert(T[][] data) { + Collection> dataCollection = new ArrayList>( + data.length); + for (T pieceOfData[] : data) { + dataCollection.add(Arrays.asList(pieceOfData)); + } + + return dataCollection; + } +}