update jexer
authorNiki Roo <niki@nikiroo.be>
Tue, 12 Mar 2019 17:49:52 +0000 (18:49 +0100)
committerNiki Roo <niki@nikiroo.be>
Tue, 12 Mar 2019 17:49:52 +0000 (18:49 +0100)
12 files changed:
libs/jexer-0.0.6-dev-sources.jar [deleted file]
libs/jexer-0.3.1-Gitlab_2019-03-09-niki1-sources.jar [new file with mode: 0644]
src/be/nikiroo/fanfix/reader/tui/TuiReaderMainWindow.java
src/be/nikiroo/fanfix/reader/tui/TuiReaderStoryWindow.java
src/be/nikiroo/jexer/TBrowsableWidget.java [new file with mode: 0644]
src/be/nikiroo/jexer/TTable.java [new file with mode: 0644]
src/be/nikiroo/jexer/TTableCellRenderer.java [new file with mode: 0644]
src/be/nikiroo/jexer/TTableCellRendererText.java [new file with mode: 0644]
src/be/nikiroo/jexer/TTableCellRendererWidget.java [new file with mode: 0644]
src/be/nikiroo/jexer/TTableColumn.java [new file with mode: 0644]
src/be/nikiroo/jexer/TTableLine.java [new file with mode: 0644]
src/be/nikiroo/jexer/TTableModel.java [new file with mode: 0644]

diff --git a/libs/jexer-0.0.6-dev-sources.jar b/libs/jexer-0.0.6-dev-sources.jar
deleted file mode 100644 (file)
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 (file)
index 0000000..0e9c0c3
Binary files /dev/null and b/libs/jexer-0.3.1-Gitlab_2019-03-09-niki1-sources.jar differ
index c10317167189e53256a6c6cae8d83cc75cbfe6f1..76c05d0cff6a4f72c3c5bfbd68daf5727c2f1bd9 100644 (file)
@@ -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();
index d4fac73a166594be5ee5d25ff8dde9ab6ddbc922..dc0afc245c9a8a1fb42401a5651830fe15dba42b 100644 (file)
@@ -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 (file)
index 0000000..aa18d09
--- /dev/null
@@ -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).
+        * <p>
+        * 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).
+        * <p>
+        * 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 (file)
index 0000000..45e5df2
--- /dev/null
@@ -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.
+ * <p>
+ * 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
+ * <tt>(raw,column)</tt>).
+ * 
+ * @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<TTableColumn> columns = new ArrayList<TTableColumn>();
+       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<? extends Object> 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.
+        * <p>
+        * 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<TTableColumn> 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.
+        * <p>
+        * 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<? extends Object> headers) {
+               setHeaders(headers, showHeader);
+       }
+
+       /**
+        * Change the headers of the table.
+        * <p>
+        * 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<? extends Object> headers, boolean showHeader) {
+               if (headers == null) {
+                       headers = new ArrayList<Object>();
+               }
+
+               int i = 0;
+               this.columns = new ArrayList<TTableColumn>();
+               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<? extends Collection<? extends Object>> 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}.
+        * <p>
+        * It will not affect the headers.
+        * <p>
+        * 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 <b>MUST</b> 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 (file)
index 0000000..6d7b3b3
--- /dev/null
@@ -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.
+ * <p>
+ * 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 (file)
index 0000000..8f81883
--- /dev/null
@@ -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}.
+ * <p>
+ * 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 (file)
index 0000000..22c6f47
--- /dev/null
@@ -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}.
+ * <p>
+ * It supports a few different modes, see
+ * {@link TTableSimpleTextCellRenderer.CellRendererMode}.
+ * 
+ * @author niki
+ */
+public class TTableCellRendererWidget extends TTableCellRenderer {
+       private boolean rightAlign;
+       private Map<String, TWidget> widgets = new HashMap<String, TWidget>();
+
+       /**
+        * 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 (file)
index 0000000..3eea230
--- /dev/null
@@ -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.
+        * <p>
+        * 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 &lt; 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 (file)
index 0000000..f393621
--- /dev/null
@@ -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<String> {
+       //TODO: in TTable: default to header of size 1
+       private List<String> list;
+
+       public TTableLine(List<String> list) {
+               this.list = list;
+       }
+
+       // TODO: override this and the rest shall follow
+       protected List<String> 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<String> iterator() {
+               return getList().iterator();
+       }
+
+       @Override
+       public Object[] toArray() {
+               return getList().toArray();
+       }
+
+       @Override
+       public <T> 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<String> subList(int fromIndex, int toIndex) {
+               return getList().subList(fromIndex, toIndex);
+       }
+
+       @Override
+       public ListIterator<String> listIterator() {
+               return getList().listIterator();
+       }
+
+       @Override
+       public ListIterator<String> 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<? extends String> c) {
+               throw new UnsupportedOperationException("Read-only collection");
+       }
+
+       @Override
+       public boolean addAll(int index, Collection<? extends String> 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 (file)
index 0000000..cd86d35
--- /dev/null
@@ -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.
+ * <p>
+ * 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}).
+ * <p>
+ * 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<? extends Collection<? extends Object>> data) {
+
+               int maxItemsPerRow = 0;
+               for (Collection<? extends Object> rowOfData : data) {
+                       maxItemsPerRow = Math.max(maxItemsPerRow, rowOfData.size());
+               }
+
+               int i = 0;
+               final Object[][] odata = new Object[data.size()][maxItemsPerRow];
+               for (Collection<? extends Object> 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 <T>
+        * 
+        * @param data
+        *            the data
+        * 
+        * @return the data in another format
+        */
+       static <T> Collection<Collection<T>> convert(T[][] data) {
+               Collection<Collection<T>> dataCollection = new ArrayList<Collection<T>>(
+                               data.length);
+               for (T pieceOfData[] : data) {
+                       dataCollection.add(Arrays.asList(pieceOfData));
+               }
+
+               return dataCollection;
+       }
+}