Merge branch 'master' of github.com:nikiroo/fanfix
authorNiki Roo <niki@nikiroo.be>
Tue, 12 Mar 2019 18:50:04 +0000 (19:50 +0100)
committerNiki Roo <niki@nikiroo.be>
Tue, 12 Mar 2019 18:50:04 +0000 (19:50 +0100)
18 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/library/LocalLibrary.java
src/be/nikiroo/fanfix/reader/BasicReader.java
src/be/nikiroo/fanfix/reader/tui/TSizeConstraint.java [new file with mode: 0644]
src/be/nikiroo/fanfix/reader/tui/TuiReaderApplication.java
src/be/nikiroo/fanfix/reader/tui/TuiReaderMainWindow.java
src/be/nikiroo/fanfix/reader/tui/TuiReaderStoryWindow.java
src/be/nikiroo/fanfix/reader/ui/GuiReaderBook.java
src/be/nikiroo/fanfix/test/ConversionTest.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 584fa1ed2c7b9834d2afba224fef96dad537d95b..50f9ac01fe902f272255e96da1facbd117a85178 100644 (file)
@@ -224,18 +224,23 @@ public class LocalLibrary extends BasicLibrary {
                File coverDir = new File(baseDir, source);
                if (coverDir.isDirectory()) {
                        File cover = new File(coverDir, ".cover.png");
-                       InputStream in;
-                       try {
-                               in = new FileInputStream(cover);
+                       if (cover.exists()) {
+                               InputStream in;
                                try {
-                                       sourceCovers.put(source, new Image(in));
-                               } finally {
-                                       in.close();
+                                       in = new FileInputStream(cover);
+                                       try {
+                                               sourceCovers.put(source, new Image(in));
+                                       } finally {
+                                               in.close();
+                                       }
+                               } catch (FileNotFoundException e) {
+                                       e.printStackTrace();
+                               } catch (IOException e) {
+                                       Instance.getTraceHandler().error(
+                                                       new IOException(
+                                                                       "Cannot load the existing custom source cover: "
+                                                                                       + cover, e));
                                }
-                       } catch (FileNotFoundException e) {
-                               e.printStackTrace();
-                       } catch (IOException e) {
-                               e.printStackTrace();
                        }
                }
 
index 8285edb47e74266bc747662fb3bbec87b5ae5460..778b6338b108f847bea745fbe8fe04c77a0454ec 100644 (file)
@@ -91,18 +91,18 @@ public abstract class BasicReader implements Reader {
        }
 
        @Override
-       public synchronized void setMeta(URL source, Progress pg)
+       public synchronized void setMeta(URL url, Progress pg)
                        throws IOException {
-               BasicSupport support = BasicSupport.getSupport(source);
+               BasicSupport support = BasicSupport.getSupport(url);
                if (support == null) {
-                       throw new IOException("URL not supported: " + source.toString());
+                       throw new IOException("URL not supported: " + url.toString());
                }
 
                story = support.process(pg);
                if (story == null) {
                        throw new IOException(
                                        "Cannot retrieve story from external source: "
-                                                       + source.toString());
+                                                       + url.toString());
                }
 
                meta = story.getMeta();
diff --git a/src/be/nikiroo/fanfix/reader/tui/TSizeConstraint.java b/src/be/nikiroo/fanfix/reader/tui/TSizeConstraint.java
new file mode 100644 (file)
index 0000000..859a6c3
--- /dev/null
@@ -0,0 +1,90 @@
+package be.nikiroo.fanfix.reader.tui;
+
+import java.util.List;
+
+import jexer.TScrollableWidget;
+import jexer.TWidget;
+import jexer.event.TResizeEvent;
+import jexer.event.TResizeEvent.Type;
+
+public class TSizeConstraint {
+       private TWidget widget;
+       private Integer x1;
+       private Integer y1;
+       private Integer x2;
+       private Integer y2;
+
+       // TODO: include in the window classes I use?
+
+       public TSizeConstraint(TWidget widget, Integer x1, Integer y1, Integer x2,
+                       Integer y2) {
+               this.widget = widget;
+               this.x1 = x1;
+               this.y1 = y1;
+               this.x2 = x2;
+               this.y2 = y2;
+       }
+
+       public TWidget getWidget() {
+               return widget;
+       }
+
+       public Integer getX1() {
+               if (x1 != null && x1 < 0)
+                       return widget.getParent().getWidth() + x1;
+               return x1;
+       }
+
+       public Integer getY1() {
+               if (y1 != null && y1 < 0)
+                       return widget.getParent().getHeight() + y1;
+               return y1;
+       }
+
+       public Integer getX2() {
+               if (x2 != null && x2 <= 0)
+                       return widget.getParent().getWidth() - 2 + x2;
+               return x2;
+       }
+
+       public Integer getY2() {
+               if (y2 != null && y2 <= 0)
+                       return widget.getParent().getHeight() - 2 + y2;
+               return y2;
+       }
+
+       // coordinates < 0 = from the other side, x2 or y2 = 0 = max size
+       static void setSize(List<TSizeConstraint> sizeConstraints, TWidget child,
+                       Integer x1, Integer y1, Integer x2, Integer y2) {
+               sizeConstraints.add(new TSizeConstraint(child, x1, y1, x2, y2));
+       }
+
+       static void resize(List<TSizeConstraint> sizeConstraints) {
+               for (TSizeConstraint sizeConstraint : sizeConstraints) {
+                       TWidget widget = sizeConstraint.getWidget();
+                       Integer x1 = sizeConstraint.getX1();
+                       Integer y1 = sizeConstraint.getY1();
+                       Integer x2 = sizeConstraint.getX2();
+                       Integer y2 = sizeConstraint.getY2();
+
+                       if (x1 != null)
+                               widget.setX(x1);
+                       if (y1 != null)
+                               widget.setY(y1);
+
+                       if (x2 != null)
+                               widget.setWidth(x2 - widget.getX());
+                       if (y2 != null)
+                               widget.setHeight(y2 - widget.getY());
+
+                       // Resize the text field
+                       // TODO: why setW/setH/reflow not enough for the scrollbars?
+                       widget.onResize(new TResizeEvent(Type.WIDGET, widget.getWidth(),
+                                       widget.getHeight()));
+
+                       if (widget instanceof TScrollableWidget) {
+                               ((TScrollableWidget) widget).reflowData();
+                       }
+               }
+       }
+}
index 1de62a46a2a420484cf7ea8fa54efee1d83cc692..52bc531ad803202904f9e78076d5da5df837b7c2 100644 (file)
@@ -4,6 +4,7 @@ import java.awt.Toolkit;
 import java.awt.datatransfer.DataFlavor;
 import java.io.IOException;
 import java.net.URL;
+import java.net.UnknownHostException;
 
 import jexer.TApplication;
 import jexer.TCommand;
@@ -12,6 +13,7 @@ import jexer.TMessageBox;
 import jexer.TStatusBar;
 import jexer.TWidget;
 import jexer.TWindow;
+import jexer.event.TCommandEvent;
 import jexer.event.TMenuEvent;
 import jexer.menu.TMenu;
 import be.nikiroo.fanfix.Instance;
@@ -20,6 +22,7 @@ import be.nikiroo.fanfix.data.Story;
 import be.nikiroo.fanfix.library.BasicLibrary;
 import be.nikiroo.fanfix.reader.BasicReader;
 import be.nikiroo.fanfix.reader.Reader;
+import be.nikiroo.fanfix.reader.tui.TuiReaderMainWindow.Mode;
 import be.nikiroo.utils.Progress;
 
 /**
@@ -44,17 +47,16 @@ class TuiReaderApplication extends TApplication implements Reader {
        private Reader reader;
        private TuiReaderMainWindow main;
 
-       private MetaData meta;
-       private String source;
-       private boolean useMeta;
-
        // start reading if meta present
        public TuiReaderApplication(Reader reader, BackendType backend)
                        throws Exception {
                super(backend);
                init(reader);
 
-               showMain(getMeta(), null, true);
+               MetaData meta = getMeta();
+               if (meta != null) {
+                       read();
+               }
        }
 
        public TuiReaderApplication(Reader reader, String source,
@@ -62,30 +64,13 @@ class TuiReaderApplication extends TApplication implements Reader {
                super(backend);
                init(reader);
 
-               showMain(null, source, false);
+               showMain();
+               main.setMode(Mode.SOURCE, source);
        }
 
        @Override
        public void read() throws IOException {
-               MetaData meta = getMeta();
-
-               if (meta == null) {
-                       throw new IOException("No story to read");
-               }
-
-               // TODO: open in editor + external option
-               if (!meta.isImageDocument()) {
-                       TWindow window = new TuiReaderStoryWindow(this, getLibrary(), meta,
-                                       getChapter());
-                       window.maximize();
-               } else {
-                       try {
-                               openExternal(getLibrary(), meta.getLuid());
-                       } catch (IOException e) {
-                               messageBox("Error when trying to open the story",
-                                               e.getMessage(), TMessageBox.Type.OK);
-                       }
-               }
+               read(getStory(null));
        }
 
        @Override
@@ -138,6 +123,25 @@ class TuiReaderApplication extends TApplication implements Reader {
                reader.setChapter(chapter);
        }
 
+       public void read(Story story) throws IOException {
+               if (story == null) {
+                       throw new IOException("No story to read");
+               }
+
+               // TODO: open in editor + external option
+               if (!story.getMeta().isImageDocument()) {
+                       TWindow window = new TuiReaderStoryWindow(this, story, getChapter());
+                       window.maximize();
+               } else {
+                       try {
+                               openExternal(getLibrary(), story.getMeta().getLuid());
+                       } catch (IOException e) {
+                               messageBox("Error when trying to open the story",
+                                               e.getMessage(), TMessageBox.Type.OK);
+                       }
+               }
+       }
+
        /**
         * Set the default status bar when this window appear.
         * <p>
@@ -158,13 +162,7 @@ class TuiReaderApplication extends TApplication implements Reader {
 
        }
 
-       private void showMain(MetaData meta, String source, boolean useMeta)
-                       throws IOException {
-               // TODO: thread-safety
-               this.meta = meta;
-               this.source = source;
-               this.useMeta = useMeta;
-
+       private void showMain() {
                if (main != null && main.isVisible()) {
                        main.activate();
                } else {
@@ -172,14 +170,7 @@ class TuiReaderApplication extends TApplication implements Reader {
                                main.close();
                        }
                        main = new TuiReaderMainWindow(this);
-                       if (useMeta) {
-                               main.setMeta(meta);
-                               if (meta != null) {
-                                       read();
-                               }
-                       } else {
-                               main.setSource(source);
-                       }
+                       main.maximize();
                }
        }
 
@@ -214,12 +205,35 @@ class TuiReaderApplication extends TApplication implements Reader {
                getBackend().setTitle("Fanfix");
        }
 
+       @Override
+       protected boolean onCommand(TCommandEvent command) {
+               if (command.getCmd().equals(TuiReaderMainWindow.CMD_SEARCH)) {
+                       messageBox("title", "caption");
+                       return true;
+               }
+               return super.onCommand(command);
+       }
+
        @Override
        protected boolean onMenu(TMenuEvent menu) {
                // TODO: i18n
                switch (menu.getId()) {
                case MENU_EXIT:
                        close(this);
+                       return true;
+               case MENU_OPEN:
+                       String openfile = null;
+                       try {
+                               openfile = fileOpenBox(".");
+                               reader.setMeta(BasicReader.getUrl(openfile), null);
+                               read();
+                       } catch (IOException e) {
+                               // TODO: i18n
+                               error("Fail to open file"
+                                               + (openfile == null ? "" : ": " + openfile),
+                                               "Import error", e);
+                       }
+
                        return true;
                case MENU_IMPORT_URL:
                        String clipboard = "";
@@ -238,42 +252,58 @@ class TuiReaderApplication extends TApplication implements Reader {
                        String url = inputBox("Import story", "URL to import", clipboard)
                                        .getText();
 
-                       if (!imprt(url)) {
-                               // TODO: bad import
+                       try {
+                               if (!imprt(url)) {
+                                       // TODO: i18n
+                                       error("URK not supported: " + url, "Import error");
+                               }
+                       } catch (IOException e) {
+                               // TODO: i18n
+                               error("Fail to import URL: " + url, "Import error", e);
                        }
 
                        return true;
                case MENU_IMPORT_FILE:
+                       String filename = null;
                        try {
-                               String filename = fileOpenBox(".");
+                               filename = fileOpenBox(".");
                                if (!imprt(filename)) {
-                                       // TODO: bad import
+                                       // TODO: i18n
+                                       error("File not supported: " + filename, "Import error");
                                }
                        } catch (IOException e) {
-                               // TODO: bad file
-                               e.printStackTrace();
+                               // TODO: i18n
+                               error("Fail to import file"
+                                               + (filename == null ? "" : ": " + filename),
+                                               "Import error", e);
                        }
-
                        return true;
                case MENU_LIBRARY:
-                       try {
-                               showMain(meta, source, useMeta);
-                       } catch (IOException e) {
-                               e.printStackTrace();
-                       }
-
+                       showMain();
                        return true;
                }
 
                return super.onMenu(menu);
        }
 
-       private boolean imprt(String url) {
+       /**
+        * Import the given url.
+        * <p>
+        * Can fail if the host is not supported.
+        * 
+        * @param url
+        * 
+        * @return TRUE in case of success, FALSE if the host is not supported
+        * 
+        * @throws IOException
+        *             in case of I/O error
+        */
+       private boolean imprt(String url) throws IOException {
                try {
                        reader.getLibrary().imprt(BasicReader.getUrl(url), null);
                        main.refreshStories();
                        return true;
-               } catch (IOException e) {
+               } catch (UnknownHostException e) {
                        return false;
                }
        }
@@ -283,6 +313,43 @@ class TuiReaderApplication extends TApplication implements Reader {
                reader.openExternal(lib, luid);
        }
 
+       /**
+        * Display an error message and log it.
+        * 
+        * @param message
+        *            the message
+        * @param title
+        *            the title of the error message
+        */
+       private void error(String message, String title) {
+               error(message, title, null);
+       }
+
+       /**
+        * Display an error message and log it, including the linked
+        * {@link Exception}.
+        * 
+        * @param message
+        *            the message
+        * @param title
+        *            the title of the error message
+        * @param e
+        *            the exception to log if any (can be NULL)
+        */
+       private void error(String message, String title, Exception e) {
+               Instance.getTraceHandler().error(title + ": " + message);
+               if (e != null) {
+                       Instance.getTraceHandler().error(e);
+               }
+
+               if (e != null) {
+                       messageBox(title, message //
+                                       + "\n" + e.getMessage());
+               } else {
+                       messageBox(title, message);
+               }
+       }
+
        /**
         * Ask the user and, if confirmed, close the {@link TApplication} this
         * {@link TWidget} is running on.
index 18a1223bbe70ea49a2ce9b5bddc244b0fe6c1da8..6523cfa0eab33e244cd39d54bfbfbcc350b9e9a1 100644 (file)
@@ -5,11 +5,19 @@ import java.util.ArrayList;
 import java.util.List;
 
 import jexer.TAction;
+import jexer.TComboBox;
+import jexer.TCommand;
+import jexer.TField;
 import jexer.TFileOpenBox.Type;
+import jexer.TKeypress;
+import jexer.TLabel;
 import jexer.TList;
+import jexer.TStatusBar;
 import jexer.TWindow;
 import jexer.event.TCommandEvent;
+import jexer.event.TKeypressEvent;
 import jexer.event.TMenuEvent;
+import jexer.event.TResizeEvent;
 import be.nikiroo.fanfix.Instance;
 import be.nikiroo.fanfix.data.MetaData;
 import be.nikiroo.fanfix.library.BasicLibrary;
@@ -23,11 +31,28 @@ import be.nikiroo.fanfix.reader.Reader;
  * @author niki
  */
 class TuiReaderMainWindow extends TWindow {
+       public static final int MENU_SEARCH = 1100;
+       public static final TCommand CMD_SEARCH = new TCommand(MENU_SEARCH) {
+       };
+
+       public enum Mode {
+               SOURCE, AUTHOR,
+       }
+
        private TList list;
        private List<MetaData> listKeys;
        private List<String> listItems;
        private Reader reader;
-       private String source;
+
+       private Mode mode = Mode.SOURCE;
+       private String target = null;
+       private String filter = "";
+
+       private List<TSizeConstraint> sizeConstraints = new ArrayList<TSizeConstraint>();
+
+       // The 2 comboboxes used to select by source/author
+       private TComboBox selectTargetBox;
+       private TComboBox selectBox;
 
        /**
         * Create a new {@link TuiReaderMainWindow} without any stories in the list.
@@ -42,22 +67,17 @@ class TuiReaderMainWindow extends TWindow {
 
                this.reader = reader;
 
-               maximize();
-
                listKeys = new ArrayList<MetaData>();
                listItems = new ArrayList<String>();
-               list = addList(listItems, 0, 0, getWidth(), getHeight(), new TAction() {
-                       @Override
-                       public void DO() {
-                               MetaData meta = getSelectedMeta();
-                               if (meta != null) {
-                                       readStory(meta);
-                               }
-                       }
-               });
 
-               // TODO: add the current "source/type" or filter
-               reader.setStatusBar(this, "Library");
+               addList();
+               addSearch();
+               addSelect();
+
+               TStatusBar statusBar = reader.setStatusBar(this, "Library");
+               statusBar.addShortcutKeypress(TKeypress.kbCtrlF, CMD_SEARCH, "Search");
+
+               TSizeConstraint.resize(sizeConstraints);
 
                // TODO: remove when not used anymore
 
@@ -84,6 +104,120 @@ class TuiReaderMainWindow extends TWindow {
                // root.addChild("child 2").addChild("sub child");
        }
 
+       private void addSearch() {
+               TLabel lblSearch = addLabel("Search: ", 0, 0);
+
+               TField search = new TField(this, 0, 0, 1, true) {
+                       @Override
+                       public void onKeypress(TKeypressEvent keypress) {
+                               super.onKeypress(keypress);
+                               TKeypress key = keypress.getKey();
+                               if (key.isFnKey() && key.getKeyCode() == TKeypress.ENTER) {
+                                       TuiReaderMainWindow.this.filter = getText();
+                                       TuiReaderMainWindow.this.refreshStories();
+                               }
+                       }
+               };
+
+               TSizeConstraint.setSize(sizeConstraints, lblSearch, 5, 1, null, null);
+               TSizeConstraint.setSize(sizeConstraints, search, 15, 1, -5, null);
+       }
+
+       private void addList() {
+               list = addList(listItems, 0, 0, 10, 10, new TAction() {
+                       @Override
+                       public void DO() {
+                               MetaData meta = getSelectedMeta();
+                               if (meta != null) {
+                                       readStory(meta);
+                               }
+                       }
+               });
+
+               TSizeConstraint.setSize(sizeConstraints, list, 0, 7, 0, 0);
+       }
+
+       private void addSelect() {
+               // TODO: i18n
+               final List<String> selects = new ArrayList<String>();
+               selects.add("(show all)");
+               selects.add("Sources");
+               selects.add("Author");
+
+               final List<String> selectTargets = new ArrayList<String>();
+               selectTargets.add("");
+
+               TLabel lblSelect = addLabel("Select: ", 0, 0);
+
+               TAction onSelect = new TAction() {
+                       @Override
+                       public void DO() {
+                               String smode = selectBox.getText();
+                               boolean showTarget;
+                               if (smode == null || smode.equals("(show all)")) {
+                                       showTarget = false;
+                               } else if (smode.equals("Sources")) {
+                                       selectTargets.clear();
+                                       selectTargets.add("(show all)");
+                                       for (String source : reader.getLibrary().getSources()) {
+                                               selectTargets.add(source);
+                                       }
+                                       showTarget = true;
+                               } else {
+                                       selectTargets.clear();
+                                       selectTargets.add("(show all)");
+                                       for (String author : reader.getLibrary().getAuthors()) {
+                                               selectTargets.add(author);
+                                       }
+
+                                       showTarget = true;
+                               }
+
+                               selectTargetBox.setVisible(showTarget);
+                               selectTargetBox.setEnabled(showTarget);
+                               if (showTarget) {
+                                       selectTargetBox.reflowData();
+                               }
+
+                               selectTargetBox.setText(selectTargets.get(0));
+                               if (showTarget) {
+                                       TuiReaderMainWindow.this.activate(selectTargetBox);
+                               } else {
+                                       TuiReaderMainWindow.this.activate(list);
+                               }
+                       }
+               };
+
+               selectBox = addComboBox(0, 0, 10, selects, 0, -1, onSelect);
+
+               selectTargetBox = addComboBox(0, 0, 0, selectTargets, 0, -1,
+                               new TAction() {
+                                       @Override
+                                       public void DO() {
+                                               if (selectTargetBox.getText().equals(
+                                                               selectTargets.get(0))) {
+                                                       setMode(mode, null);
+                                               } else {
+                                                       setMode(mode, selectTargetBox.getText());
+                                               }
+                                       }
+                               });
+
+               // Set defaults
+               onSelect.DO();
+
+               TSizeConstraint.setSize(sizeConstraints, lblSelect, 5, 3, null, null);
+               TSizeConstraint.setSize(sizeConstraints, selectBox, 15, 3, -5, null);
+               TSizeConstraint.setSize(sizeConstraints, selectTargetBox, 15, 4, -5,
+                               null);
+       }
+
+       @Override
+       public void onResize(TResizeEvent resize) {
+               super.onResize(resize);
+               TSizeConstraint.resize(sizeConstraints);
+       }
+
        @Override
        public void onClose() {
                setVisible(false);
@@ -91,38 +225,43 @@ class TuiReaderMainWindow extends TWindow {
        }
 
        /**
-        * Change the source filter and display all stories matching this source.
-        * 
-        * @param source
-        *            the new source or NULL for all sources
+        * Refresh the list of stories displayed in this library.
+        * <p>
+        * Will take the current settings into account (filter, source...).
         */
-       public void setSource(String source) {
-               this.source = source;
-               refreshStories();
-       }
-
        public void refreshStories() {
-               List<MetaData> metas = reader.getLibrary().getListBySource(source);
+               List<MetaData> metas;
+               if (mode == Mode.SOURCE) {
+                       metas = reader.getLibrary().getListBySource(target);
+               } else if (mode == Mode.AUTHOR) {
+                       metas = reader.getLibrary().getListByAuthor(target);
+               } else {
+                       metas = reader.getLibrary().getList();
+               }
+
                setMetas(metas);
        }
 
        /**
-        * Update the list of stories displayed in this {@link TWindow}.
+        * Change the author/source filter and display all stories matching this
+        * target.
         * 
-        * @param meta
-        *            the new (unique) story to display
+        * @param mode
+        *            the new mode or NULL for no sorting
+        * @param target
+        *            the actual target for the given mode, or NULL for all of them
         */
-       public void setMeta(MetaData meta) {
-               List<MetaData> metas = new ArrayList<MetaData>();
-               if (meta != null) {
-                       metas.add(meta);
-               }
-
-               setMetas(metas);
+       public void setMode(Mode mode, String target) {
+               this.mode = mode;
+               this.target = target;
+               refreshStories();
        }
 
        /**
         * Update the list of stories displayed in this {@link TWindow}.
+        * <p>
+        * If a filter is set, only the stories which pass the filter will be
+        * displayed.
         * 
         * @param metas
         *            the new list of stories to display
@@ -133,12 +272,19 @@ class TuiReaderMainWindow extends TWindow {
 
                if (metas != null) {
                        for (MetaData meta : metas) {
-                               listKeys.add(meta);
-                               listItems.add(desc(meta));
+                               String desc = desc(meta);
+                               if (filter.isEmpty()
+                                               || desc.toLowerCase().contains(filter.toLowerCase())) {
+                                       listKeys.add(meta);
+                                       listItems.add(desc);
+                               }
                        }
                }
 
                list.setList(listItems);
+               if (listItems.size() > 0) {
+                       list.setSelectedIndex(0);
+               }
        }
 
        public MetaData getSelectedMeta() {
@@ -196,6 +342,7 @@ class TuiReaderMainWindow extends TWindow {
                                }
 
                                return;
+
                        case -1:
                                try {
                                        reader.getLibrary().delete(meta.getLuid());
index f5a2bfa547d8804835667f5140d2bcd8f632a873..dc0afc245c9a8a1fb42401a5651830fe15dba42b 100644 (file)
@@ -10,18 +10,16 @@ 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;
 import jexer.event.TResizeEvent;
-import jexer.event.TResizeEvent.Type;
 import be.nikiroo.fanfix.data.Chapter;
 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.fanfix.library.BasicLibrary;
+import be.nikiroo.jexer.TTable;
 import be.nikiroo.utils.StringUtils;
 
 /**
@@ -31,8 +29,6 @@ import be.nikiroo.utils.StringUtils;
  * @author niki
  */
 class TuiReaderStoryWindow extends TWindow {
-       private BasicLibrary lib;
-       private MetaData meta;
        private Story story;
        private TLabel titleField;
        private TText textField;
@@ -40,53 +36,47 @@ class TuiReaderStoryWindow extends TWindow {
        private int chapter = -99; // invalid value
        private List<TButton> navigationButtons;
        private TLabel currentChapter;
+       private List<TSizeConstraint> sizeConstraints = new ArrayList<TSizeConstraint>();
 
        // chapter: -1 for "none" (0 is desc)
-       public TuiReaderStoryWindow(TuiReaderApplication app, BasicLibrary lib,
-                       MetaData meta, int chapter) {
-               super(app, desc(meta), 0, 0, 60, 18, CENTERED | RESIZABLE);
+       public TuiReaderStoryWindow(TuiReaderApplication app, Story story,
+                       int chapter) {
+               super(app, desc(story.getMeta()), 0, 0, 60, 18, CENTERED | RESIZABLE);
 
-               this.lib = lib;
-               this.meta = meta;
+               this.story = story;
 
-               app.setStatusBar(this, desc(meta));
+               app.setStatusBar(this, desc(story.getMeta()));
 
                // last = use window background
                titleField = new TLabel(this, "    Title", 0, 1, "tlabel", false);
-               textField = new TText(this, "", 1, 3, getWidth() - 4, getHeight() - 5);
-               table = new TTable(this, 0, 3, getWidth(), getHeight() - 4, null, null,
-                               Arrays.asList("Key", "Value"), true);
+               textField = new TText(this, "", 0, 0, 1, 1);
+               table = new TTable(this, 0, 0, 1, 1, null, null, Arrays.asList("Key", "Value"), true);
 
                titleField.setEnabled(false);
-               textField.getVerticalScroller().setX(
-                               textField.getVerticalScroller().getX() + 1);
 
                navigationButtons = new ArrayList<TButton>(5);
 
-               // -3 because 0-based and 2 for borders
-               int row = getHeight() - 3;
-
                // for bg colour when << button is pressed
-               navigationButtons.add(addButton(" ", 0, row, null));
-               navigationButtons.add(addButton("<<  ", 0, row, new TAction() {
+               navigationButtons.add(addButton(" ", 0, 0, null));
+               navigationButtons.add(addButton("<<  ", 0, 0, new TAction() {
                        @Override
                        public void DO() {
                                setChapter(-1);
                        }
                }));
-               navigationButtons.add(addButton("<  ", 4, row, new TAction() {
+               navigationButtons.add(addButton("<  ", 4, 0, new TAction() {
                        @Override
                        public void DO() {
                                setChapter(TuiReaderStoryWindow.this.chapter - 1);
                        }
                }));
-               navigationButtons.add(addButton(">  ", 7, row, new TAction() {
+               navigationButtons.add(addButton(">  ", 7, 0, new TAction() {
                        @Override
                        public void DO() {
                                setChapter(TuiReaderStoryWindow.this.chapter + 1);
                        }
                }));
-               navigationButtons.add(addButton(">>  ", 10, row, new TAction() {
+               navigationButtons.add(addButton(">>  ", 10, 0, new TAction() {
                        @Override
                        public void DO() {
                                setChapter(getStory().getChapters().size());
@@ -97,41 +87,38 @@ class TuiReaderStoryWindow extends TWindow {
                navigationButtons.get(1).setEnabled(false);
                navigationButtons.get(2).setEnabled(false);
 
-               currentChapter = addLabel("", 14, row);
-               currentChapter.setWidth(getWidth() - 10);
+               currentChapter = addLabel("", 0, 0);
+
+               TSizeConstraint.setSize(sizeConstraints, textField, 1, 3, -1, 0);
+               TSizeConstraint.setSize(sizeConstraints, table, 0, 3, 0, 0);
+               TSizeConstraint.setSize(sizeConstraints, currentChapter, 14, -3, -1,
+                               null);
+
+               for (TButton navigationButton : navigationButtons) {
+                       TSizeConstraint.setSize(sizeConstraints, navigationButton, null,
+                                       -3, null, null);
+               }
+
+               onResize(null);
 
                setChapter(chapter);
        }
 
        @Override
        public void onResize(TResizeEvent resize) {
-               super.onResize(resize);
-
-               // Resize the text field
-               // TODO: why setW/setH/reflow not enough for the scrollbars?
-               textField.onResize(new TResizeEvent(Type.WIDGET, resize.getWidth() - 4,
-                               resize.getHeight() - 5));
-               textField.getVerticalScroller().setX(
-                               textField.getVerticalScroller().getX() + 1);
+               if (resize != null) {
+                       super.onResize(resize);
+               }
 
-               table.setWidth(getWidth());
-               table.setHeight(getHeight() - 4);
-               table.reflowData();
+               // TODO: find out why TText and TTable does not behave the same way
+               // (offset of 2 for height and width)
 
-               // -3 because 0-based and 2 for borders
-               int row = getHeight() - 3;
+               TSizeConstraint.resize(sizeConstraints);
 
-               String name = currentChapter.getLabel();
-               while (name.length() < resize.getWidth() - currentChapter.getX()) {
-                       name += " ";
-               }
-               currentChapter.setLabel(name);
-               currentChapter.setWidth(resize.getWidth() - 10);
-               currentChapter.setY(row);
+               textField.getVerticalScroller().setX(
+                               textField.getVerticalScroller().getX() + 1);
 
-               for (TButton button : navigationButtons) {
-                       button.setY(row);
-               }
+               setCurrentChapterText();
        }
 
        /**
@@ -274,10 +261,6 @@ class TuiReaderStoryWindow extends TWindow {
        }
 
        private Story getStory() {
-               if (story == null) {
-                       // TODO: progress bar?
-                       story = lib.getStory(meta.getLuid(), null);
-               }
                return story;
        }
 
@@ -315,10 +298,7 @@ class TuiReaderStoryWindow extends TWindow {
                }
 
                int width = getWidth() - currentChapter.getX();
-               while (name.length() < width) {
-                       name += " ";
-               }
-
+               name = String.format("%-" + width + "s", name);
                if (name.length() > width) {
                        name = name.substring(0, width);
                }
index 72af3f1f9446e2e2e05e25f7424341786109faa2..dc275950c6297ae561504b18131a1853dfd1a2c5 100644 (file)
@@ -401,8 +401,6 @@ class GuiReaderBook extends JPanel {
                                                        .getSourceCover(meta.getSource());
                                }
 
-                               BufferedImage coverb = ImageUtilsAwt.fromImage(cover);
-
                                resizedImage = new BufferedImage(SPINE_WIDTH + COVER_WIDTH,
                                                SPINE_HEIGHT + COVER_HEIGHT + HOFFSET,
                                                BufferedImage.TYPE_4BYTE_ABGR);
@@ -410,6 +408,7 @@ class GuiReaderBook extends JPanel {
                                g.setColor(Color.white);
                                g.fillRect(0, HOFFSET, COVER_WIDTH, COVER_HEIGHT);
                                if (cover != null) {
+                                       BufferedImage coverb = ImageUtilsAwt.fromImage(cover);
                                        g.drawImage(coverb, 0, HOFFSET, COVER_WIDTH, COVER_HEIGHT,
                                                        null);
                                } else {
index 51b6deb4f817e6a2f451ead2d7b73f554a7795af..0eb1eb04b95eef90cbd749901d673a966bee3144 100644 (file)
@@ -172,10 +172,16 @@ class ConversionTest extends TestLauncher {
                        };
                }
 
-               List<String> resultFiles = Arrays.asList(resultDir.list(filter));
-               resultFiles.sort(null);
-               List<String> expectedFiles = Arrays.asList(expectedDir.list(filter));
-               expectedFiles.sort(null);
+               List<String> resultFiles;
+               List<String> expectedFiles;
+               {
+                       String[] resultArr = resultDir.list(filter);
+                       Arrays.sort(resultArr);
+                       resultFiles = Arrays.asList(resultArr);
+                       String[] expectedArr = expectedDir.list(filter);
+                       Arrays.sort(expectedArr);
+                       expectedFiles = Arrays.asList(expectedArr);
+               }
 
                testCase.assertEquals(errMess, expectedFiles, resultFiles);
 
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;
+       }
+}