From: Niki Roo Date: Tue, 12 Mar 2019 18:50:04 +0000 (+0100) Subject: Merge branch 'master' of github.com:nikiroo/fanfix X-Git-Tag: fanfix-2.0.0~62 X-Git-Url: http://git.nikiroo.be/?p=fanfix.git;a=commitdiff_plain;h=d5a3b60634af7e3cbd0b698e6369072751ee1518;hp=7c0ab239a1bb31ab020ec3d7a7960b1f50216ccc Merge branch 'master' of github.com:nikiroo/fanfix --- diff --git a/libs/jexer-0.0.6-dev-sources.jar b/libs/jexer-0.0.6-dev-sources.jar deleted file mode 100644 index 5fb7f2d..0000000 Binary files a/libs/jexer-0.0.6-dev-sources.jar and /dev/null differ diff --git a/libs/jexer-0.3.1-Gitlab_2019-03-09-niki1-sources.jar b/libs/jexer-0.3.1-Gitlab_2019-03-09-niki1-sources.jar new file mode 100644 index 0000000..0e9c0c3 Binary files /dev/null and b/libs/jexer-0.3.1-Gitlab_2019-03-09-niki1-sources.jar differ diff --git a/src/be/nikiroo/fanfix/library/LocalLibrary.java b/src/be/nikiroo/fanfix/library/LocalLibrary.java index 584fa1e..50f9ac0 100644 --- a/src/be/nikiroo/fanfix/library/LocalLibrary.java +++ b/src/be/nikiroo/fanfix/library/LocalLibrary.java @@ -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(); } } diff --git a/src/be/nikiroo/fanfix/reader/BasicReader.java b/src/be/nikiroo/fanfix/reader/BasicReader.java index 8285edb..778b633 100644 --- a/src/be/nikiroo/fanfix/reader/BasicReader.java +++ b/src/be/nikiroo/fanfix/reader/BasicReader.java @@ -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 index 0000000..859a6c3 --- /dev/null +++ b/src/be/nikiroo/fanfix/reader/tui/TSizeConstraint.java @@ -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 sizeConstraints, TWidget child, + Integer x1, Integer y1, Integer x2, Integer y2) { + sizeConstraints.add(new TSizeConstraint(child, x1, y1, x2, y2)); + } + + static void resize(List 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(); + } + } + } +} diff --git a/src/be/nikiroo/fanfix/reader/tui/TuiReaderApplication.java b/src/be/nikiroo/fanfix/reader/tui/TuiReaderApplication.java index 1de62a4..52bc531 100644 --- a/src/be/nikiroo/fanfix/reader/tui/TuiReaderApplication.java +++ b/src/be/nikiroo/fanfix/reader/tui/TuiReaderApplication.java @@ -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. *

@@ -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. + *

+ * 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. diff --git a/src/be/nikiroo/fanfix/reader/tui/TuiReaderMainWindow.java b/src/be/nikiroo/fanfix/reader/tui/TuiReaderMainWindow.java index 18a1223..6523cfa 100644 --- a/src/be/nikiroo/fanfix/reader/tui/TuiReaderMainWindow.java +++ b/src/be/nikiroo/fanfix/reader/tui/TuiReaderMainWindow.java @@ -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 listKeys; private List listItems; private Reader reader; - private String source; + + private Mode mode = Mode.SOURCE; + private String target = null; + private String filter = ""; + + private List sizeConstraints = new ArrayList(); + + // 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(); listItems = new ArrayList(); - 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 selects = new ArrayList(); + selects.add("(show all)"); + selects.add("Sources"); + selects.add("Author"); + + final List selectTargets = new ArrayList(); + 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. + *

+ * Will take the current settings into account (filter, source...). */ - public void setSource(String source) { - this.source = source; - refreshStories(); - } - public void refreshStories() { - List metas = reader.getLibrary().getListBySource(source); + List 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 metas = new ArrayList(); - 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}. + *

+ * 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()); diff --git a/src/be/nikiroo/fanfix/reader/tui/TuiReaderStoryWindow.java b/src/be/nikiroo/fanfix/reader/tui/TuiReaderStoryWindow.java index f5a2bfa..dc0afc2 100644 --- a/src/be/nikiroo/fanfix/reader/tui/TuiReaderStoryWindow.java +++ b/src/be/nikiroo/fanfix/reader/tui/TuiReaderStoryWindow.java @@ -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 navigationButtons; private TLabel currentChapter; + private List sizeConstraints = new ArrayList(); // 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(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); } diff --git a/src/be/nikiroo/fanfix/reader/ui/GuiReaderBook.java b/src/be/nikiroo/fanfix/reader/ui/GuiReaderBook.java index 72af3f1..dc27595 100644 --- a/src/be/nikiroo/fanfix/reader/ui/GuiReaderBook.java +++ b/src/be/nikiroo/fanfix/reader/ui/GuiReaderBook.java @@ -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 { diff --git a/src/be/nikiroo/fanfix/test/ConversionTest.java b/src/be/nikiroo/fanfix/test/ConversionTest.java index 51b6deb..0eb1eb0 100644 --- a/src/be/nikiroo/fanfix/test/ConversionTest.java +++ b/src/be/nikiroo/fanfix/test/ConversionTest.java @@ -172,10 +172,16 @@ class ConversionTest extends TestLauncher { }; } - List resultFiles = Arrays.asList(resultDir.list(filter)); - resultFiles.sort(null); - List expectedFiles = Arrays.asList(expectedDir.list(filter)); - expectedFiles.sort(null); + List resultFiles; + List 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 index 0000000..aa18d09 --- /dev/null +++ b/src/be/nikiroo/jexer/TBrowsableWidget.java @@ -0,0 +1,404 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2019 David "Niki" ROULET + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author David ROULET [niki@nikiroo.be] + * @version 1 + */ +package be.nikiroo.jexer; + +import static jexer.TKeypress.kbBackTab; +import static jexer.TKeypress.kbDown; +import static jexer.TKeypress.kbEnd; +import static jexer.TKeypress.kbEnter; +import static jexer.TKeypress.kbHome; +import static jexer.TKeypress.kbLeft; +import static jexer.TKeypress.kbPgDn; +import static jexer.TKeypress.kbPgUp; +import static jexer.TKeypress.kbRight; +import static jexer.TKeypress.kbShiftTab; +import static jexer.TKeypress.kbTab; +import static jexer.TKeypress.kbUp; +import jexer.THScroller; +import jexer.TScrollableWidget; +import jexer.TVScroller; +import jexer.TWidget; +import jexer.event.TKeypressEvent; +import jexer.event.TMouseEvent; +import jexer.event.TResizeEvent; + +/** + * This class represents a browsable {@link TWidget}, that is, a {@link TWidget} + * where you can use the keyboard or mouse to browse to one line to the next, or + * from left t right. + * + * @author niki + */ +abstract public class TBrowsableWidget extends TScrollableWidget { + private int selectedRow; + private int selectedColumn; + private int yOffset; + + /** + * The number of rows in this {@link TWidget}. + * + * @return the number of rows + */ + abstract protected int getRowCount(); + + /** + * The number of columns in this {@link TWidget}. + * + * @return the number of columns + */ + abstract protected int getColumnCount(); + + /** + * The virtual width of this {@link TWidget}, that is, the total width it + * can take to display all the data. + * + * @return the width + */ + abstract int getVirtualWidth(); + + /** + * The virtual height of this {@link TWidget}, that is, the total width it + * can take to display all the data. + * + * @return the height + */ + abstract int getVirtualHeight(); + + /** + * Basic setup of this class (called by all constructors) + */ + private void setup() { + vScroller = new TVScroller(this, 0, 0, 1); + hScroller = new THScroller(this, 0, 0, 1); + fixScrollers(); + } + + /** + * Create a new {@link TBrowsableWidget} linked to the given {@link TWidget} + * parent. + * + * @param parent + * parent widget + */ + protected TBrowsableWidget(final TWidget parent) { + super(parent); + setup(); + } + + /** + * Create a new {@link TBrowsableWidget} linked to the given {@link TWidget} + * parent. + * + * @param parent + * parent widget + * @param x + * column relative to parent + * @param y + * row relative to parent + * @param width + * width of widget + * @param height + * height of widget + */ + protected TBrowsableWidget(final TWidget parent, final int x, final int y, + final int width, final int height) { + super(parent, x, y, width, height); + setup(); + } + + /** + * Create a new {@link TBrowsableWidget} linked to the given {@link TWidget} + * parent. + * + * @param parent + * parent widget + * @param enabled + * if true assume enabled + */ + protected TBrowsableWidget(final TWidget parent, final boolean enabled) { + super(parent, enabled); + setup(); + } + + /** + * Create a new {@link TBrowsableWidget} linked to the given {@link TWidget} + * parent. + * + * @param parent + * parent widget + * @param enabled + * if true assume enabled + * @param x + * column relative to parent + * @param y + * row relative to parent + * @param width + * width of widget + * @param height + * height of widget + */ + protected TBrowsableWidget(final TWidget parent, final boolean enabled, + final int x, final int y, final int width, final int height) { + super(parent, enabled, x, y, width, height); + setup(); + } + + /** + * The currently selected row (or -1 if no row is selected). + * + * @return the selected row + */ + public int getSelectedRow() { + return selectedRow; + } + + /** + * The currently selected row (or -1 if no row is selected). + *

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

+ * You may want to call {@link TBrowsableWidget#reflowData()} when done to + * see the changes. + * + * @param selectedColumn + * the new selected column + * + * @throws IndexOutOfBoundsException + * when the index is out of bounds + */ + public void setSelectedColumn(int selectedColumn) { + if (selectedColumn < -1 || selectedColumn >= getColumnCount()) { + throw new IndexOutOfBoundsException(String.format( + "Cannot set column %d on a table with %d columns", + selectedColumn, getColumnCount())); + } + + this.selectedColumn = selectedColumn; + } + + /** + * An offset on the Y position of the table, i.e., the number of rows to + * skip so the control can draw that many rows always on top. + * + * @return the offset + */ + public int getYOffset() { + return yOffset; + } + + /** + * An offset on the Y position of the table, i.e., the number of rows that + * should always stay on top. + * + * @param yOffset + * the new offset + */ + public void setYOffset(int yOffset) { + this.yOffset = yOffset; + } + + @SuppressWarnings("unused") + public void dispatchMove(int fromRow, int toRow) { + reflowData(); + } + + @SuppressWarnings("unused") + public void dispatchEnter(int selectedRow) { + reflowData(); + } + + @Override + public void onMouseDown(final TMouseEvent mouse) { + if (mouse.isMouseWheelUp()) { + vScroller.decrement(); + return; + } + if (mouse.isMouseWheelDown()) { + vScroller.increment(); + return; + } + + if ((mouse.getX() < getWidth() - 1) && (mouse.getY() < getHeight() - 1)) { + if (vScroller.getValue() + mouse.getY() < getRowCount()) { + selectedRow = vScroller.getValue() + mouse.getY() + - getYOffset(); + } + dispatchEnter(selectedRow); + return; + } + + // Pass to children + super.onMouseDown(mouse); + } + + @Override + public void onKeypress(final TKeypressEvent keypress) { + int maxX = getRowCount(); + int prevSelectedRow = selectedRow; + + int firstLineIndex = vScroller.getValue() - getYOffset() + 2; + int lastLineIndex = firstLineIndex - hScroller.getHeight() + + getHeight() - 2 - 2; + + if (keypress.equals(kbLeft)) { + hScroller.decrement(); + } else if (keypress.equals(kbRight)) { + hScroller.increment(); + } else if (keypress.equals(kbUp)) { + if (maxX > 0 && selectedRow < maxX) { + if (selectedRow > 0) { + if (selectedRow <= firstLineIndex) { + vScroller.decrement(); + } + selectedRow--; + } else { + selectedRow = 0; + } + + dispatchMove(prevSelectedRow, selectedRow); + } + } else if (keypress.equals(kbDown)) { + if (maxX > 0) { + if (selectedRow >= 0) { + if (selectedRow < maxX - 1) { + selectedRow++; + if (selectedRow >= lastLineIndex) { + vScroller.increment(); + } + } + } else { + selectedRow = 0; + } + + dispatchMove(prevSelectedRow, selectedRow); + } + } else if (keypress.equals(kbPgUp)) { + if (selectedRow >= 0) { + vScroller.bigDecrement(); + selectedRow -= getHeight() - 1; + if (selectedRow < 0) { + selectedRow = 0; + } + + dispatchMove(prevSelectedRow, selectedRow); + } + } else if (keypress.equals(kbPgDn)) { + if (selectedRow >= 0) { + vScroller.bigIncrement(); + selectedRow += getHeight() - 1; + if (selectedRow > getRowCount() - 1) { + selectedRow = getRowCount() - 1; + } + + dispatchMove(prevSelectedRow, selectedRow); + } + } else if (keypress.equals(kbHome)) { + if (getRowCount() > 0) { + vScroller.toTop(); + selectedRow = 0; + dispatchMove(prevSelectedRow, selectedRow); + } + } else if (keypress.equals(kbEnd)) { + if (getRowCount() > 0) { + vScroller.toBottom(); + selectedRow = getRowCount() - 1; + dispatchMove(prevSelectedRow, selectedRow); + } + } else if (keypress.equals(kbTab)) { + getParent().switchWidget(true); + } else if (keypress.equals(kbShiftTab) || keypress.equals(kbBackTab)) { + getParent().switchWidget(false); + } else if (keypress.equals(kbEnter)) { + if (selectedRow >= 0) { + dispatchEnter(selectedRow); + } + } else { + // Pass other keys (tab etc.) on + super.onKeypress(keypress); + } + } + + @Override + public void onResize(TResizeEvent event) { + super.onResize(event); + reflowData(); + } + + @Override + public void reflowData() { + super.reflowData(); + fixScrollers(); + } + + private void fixScrollers() { + vScroller.setX(Math.max(0, getWidth() - 3)); + vScroller.setHeight(Math.max(1, getHeight() - 2)); + hScroller.setY(Math.max(0, getHeight() - 3)); + hScroller.setWidth(Math.max(1, getWidth() - 3)); + + // virtual_size + // - the other scroll bar size + // + 2 (for the border of the window) + vScroller.setTopValue(0); + vScroller.setBottomValue(Math.max(0, getVirtualHeight() - getHeight() + + hScroller.getHeight() + 2)); + hScroller.setLeftValue(0); + hScroller.setRightValue(Math.max(0, getVirtualWidth() - getWidth() + + vScroller.getWidth() + 2)); + } +} diff --git a/src/be/nikiroo/jexer/TTable.java b/src/be/nikiroo/jexer/TTable.java new file mode 100644 index 0000000..45e5df2 --- /dev/null +++ b/src/be/nikiroo/jexer/TTable.java @@ -0,0 +1,516 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2019 David "Niki" ROULET + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author David ROULET [niki@nikiroo.be] + * @version 1 + */ +package be.nikiroo.jexer; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import javax.swing.table.TableModel; + +import be.nikiroo.jexer.TTableCellRenderer.CellRendererMode; +import jexer.TAction; +import jexer.TWidget; +import jexer.bits.CellAttributes; + +/** + * A table widget to display and browse through tabular data. + *

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

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

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

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

+ * It will not affect the headers. + *

+ * You may want to call {@link TTable#reflowData()} when done to see the + * changes. + */ + public void clear() { + setSelectedRow(-1); + selectedColumn = -1; + setModel(new TTableModel(new Object[][] {})); + } + + @Override + public void reflowData() { + super.reflowData(); + + int lastAutoColumn = -1; + int rowWidth = 0; + + int i = 0; + for (TTableColumn tcol : columns) { + tcol.reflowData(); + + if (!tcol.isForcedWidth()) { + lastAutoColumn = i; + } + + rowWidth += tcol.getWidth(); + + i++; + } + + if (!columns.isEmpty()) { + rowWidth += (i - 1) * getSeparatorRenderer().getWidthOf(null); + + int extraWidth = getWidth() - rowWidth; + if (extraWidth > 0) { + if (lastAutoColumn < 0) { + lastAutoColumn = columns.size() - 1; + } + TTableColumn tcol = columns.get(lastAutoColumn); + tcol.expandWidthTo(tcol.getWidth() + extraWidth); + rowWidth += extraWidth; + } + } + } + + @Override + public void draw() { + int begin = vScroller.getValue(); + int y = this.showHeader ? 2 : 0; + + if (showHeader) { + CellAttributes colorHeaders = getHeaderRenderer() + .getCellAttributes(getTheme(), false, isAbsoluteActive()); + drawRow(-1, 0); + String formatString = "%-" + Integer.toString(getWidth()) + "s"; + String data = String.format(formatString, ""); + getScreen().putStringXY(0, 1, data, colorHeaders); + } + + // draw the actual rows until no more, + // then pad the rest with blank rows + for (int i = begin; i < getRowCount(); i++) { + drawRow(i, y); + y++; + + // -2: window borders + if (y >= getHeight() - 2 - getHorizontalScroller().getHeight()) { + break; + } + } + + CellAttributes emptyRowColor = getSeparatorRenderer() + .getCellAttributes(getTheme(), false, isAbsoluteActive()); + for (int i = getRowCount(); i < getHeight(); i++) { + getScreen().hLineXY(0, y, getWidth() - 1, ' ', emptyRowColor); + y++; + } + } + + @Override + protected int getVirtualWidth() { + int width = 0; + + if (getColumns() != null) { + for (TTableColumn tcol : getColumns()) { + width += tcol.getWidth(); + } + + if (getColumnCount() > 0) { + width += (getColumnCount() - 1) + * getSeparatorRenderer().getWidthOf(null); + } + } + + return width; + } + + @Override + protected int getVirtualHeight() { + // TODO: allow changing the height of one row + return (showHeader ? 2 : 0) + (getRowCount() * 1); + } + + /** + * Draw the given row (it MUST exist) at the specified index and + * offset. + * + * @param rowIndex + * the index of the row to draw or -1 for the headers + * @param y + * the Y position + */ + private void drawRow(int rowIndex, int y) { + for (int i = 0; i < getColumnCount(); i++) { + TTableColumn tcol = columns.get(i); + Object value; + if (rowIndex < 0) { + value = tcol.getHeaderValue(); + } else { + value = model.getValueAt(rowIndex, tcol.getModelIndex()); + } + + if (i > 0) { + TTableCellRenderer sep = rowIndex < 0 ? getHeaderSeparatorRenderer() + : getSeparatorRenderer(); + sep.renderTableCell(this, null, rowIndex, i - 1, y); + } + + if (rowIndex < 0) { + getHeaderRenderer() + .renderTableCell(this, value, rowIndex, i, y); + } else { + tcol.getRenderer().renderTableCell(this, value, rowIndex, i, y); + } + } + } +} diff --git a/src/be/nikiroo/jexer/TTableCellRenderer.java b/src/be/nikiroo/jexer/TTableCellRenderer.java new file mode 100644 index 0000000..6d7b3b3 --- /dev/null +++ b/src/be/nikiroo/jexer/TTableCellRenderer.java @@ -0,0 +1,240 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2019 David "Niki" ROULET + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author David ROULET [niki@nikiroo.be] + * @version 1 + */ +package be.nikiroo.jexer; + +import jexer.bits.CellAttributes; +import jexer.bits.ColorTheme; + +/** + * A {@link TTable} cell renderer allows you to customize the way a single cell + * will be displayed on screen. + *

+ * It can be used in a {@link TTable} for the haeders or the separators or in a + * {@link TTableColumn} for the data. + * + * @author niki + */ +abstract public class TTableCellRenderer { + private CellRendererMode mode; + + /** + * The simple renderer mode. + * + * @author niki + */ + public enum CellRendererMode { + /** Normal text mode */ + NORMAL, + /** Only display a separator */ + SEPARATOR, + /** Header text mode */ + HEADER, + /** Both HEADER and SEPARATOR at once */ + HEADER_SEPARATOR; + + /** + * This mode represents a separator. + * + * @return TRUE for separators + */ + public boolean isSeparator() { + return this == SEPARATOR || this == HEADER_SEPARATOR; + } + + /** + * This mode represents a header. + * + * @return TRUE for headers + */ + public boolean isHeader() { + return this == HEADER || this == HEADER_SEPARATOR; + } + } + + /** + * Create a new renderer of the given mode. + * + * @param mode + * the renderer mode, cannot be NULL + */ + public TTableCellRenderer(CellRendererMode mode) { + if (mode == null) { + throw new IllegalArgumentException( + "Cannot create a renderer of type NULL"); + } + + this.mode = mode; + } + + /** + * Render the given value. + * + * @param table + * the table to write on + * @param value + * the value to write + * @param rowIndex + * the row index in the table + * @param colIndex + * the column index in the table + * @param y + * the Y position at which to draw this row + */ + abstract public void renderTableCell(TTable table, Object value, + int rowIndex, int colIndex, int y); + + /** + * The mode of this {@link TTableCellRenderer}. + * + * @return the mode + */ + public CellRendererMode getMode() { + return mode; + } + + /** + * The cell attributes to use for the given state. + * + * @param theme + * the color theme to use + * @param isSelected + * TRUE if the cell is selected + * @param hasFocus + * TRUE if the cell has focus + * + * @return the attributes + */ + public CellAttributes getCellAttributes(ColorTheme theme, + boolean isSelected, boolean hasFocus) { + return theme.getColor(getColorKey(isSelected, hasFocus)); + } + + /** + * Measure the width of the value. + * + * @param value + * the value to measure + * + * @return its width + */ + public int getWidthOf(Object value) { + if (getMode().isSeparator()) { + return asText(null, 0, false).length(); + } + return ("" + value).length(); + } + + /** + * The colour to use for the given state, specified as a Jexer colour key. + * + * @param isSelected + * TRUE if the cell is selected + * @param hasFocus + * TRUE if the cell has focus + * + * @return the colour key + */ + protected String getColorKey(boolean isSelected, boolean hasFocus) { + if (mode.isHeader()) { + return "tlabel"; + } + + String colorKey = "tlist"; + if (isSelected) { + colorKey += ".selected"; + } else if (!hasFocus) { + colorKey += ".inactive"; + } + + return colorKey; + } + + /** + * Return the X offset to use to draw a column at the given index. + * + * @param table + * the table to draw into + * @param colIndex + * the column index + * + * @return the offset + */ + protected int getXOffset(TTable table, int colIndex) { + int xOffset = -table.getHorizontalValue(); + for (int i = 0; i <= colIndex; i++) { + TTableColumn tcol = table.getColumns().get(i); + xOffset += tcol.getWidth(); + if (i > 0) { + xOffset += table.getSeparatorRenderer().getWidthOf(null); + } + } + + TTableColumn tcol = table.getColumns().get(colIndex); + if (!getMode().isSeparator()) { + xOffset -= tcol.getWidth(); + } + + return xOffset; + } + + /** + * Return the text to use (usually the converted-to-text value, except for + * the special separator mode). + * + * @param value + * the value to get the text of + * @param width + * the width we should tale + * @param align + * the text to the right + * + * @return the {@link String} to display + */ + protected String asText(Object value, int width, boolean rightAlign) { + if (getMode().isSeparator()) { + // some nice characters for the separator: ┃ │ | + return " │ "; + } + + if (width <= 0) { + return ""; + } + + String format; + if (!rightAlign) { + // Left align + format = "%-" + width + "s"; + } else { + // right align + format = "%" + width + "s"; + } + + return String.format(format, value); + } +} \ No newline at end of file diff --git a/src/be/nikiroo/jexer/TTableCellRendererText.java b/src/be/nikiroo/jexer/TTableCellRendererText.java new file mode 100644 index 0000000..8f81883 --- /dev/null +++ b/src/be/nikiroo/jexer/TTableCellRendererText.java @@ -0,0 +1,91 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2019 David "Niki" ROULET + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author David ROULET [niki@nikiroo.be] + * @version 1 + */ +package be.nikiroo.jexer; + +import jexer.bits.CellAttributes; + +/** + * A simple {@link TTableCellRenderer} that display the values within a + * {@link TLabel}. + *

+ * It supports a few different modes, see + * {@link TTableOldSimpleTextCellRenderer.CellRendererMode}. + * + * @author niki + */ +public class TTableCellRendererText extends TTableCellRenderer { + private boolean rightAlign; + + /** + * Create a new renderer for normal text mode. + */ + public TTableCellRendererText() { + this(CellRendererMode.NORMAL); + } + + /** + * Create a new renderer of the given mode. + * + * @param mode + * the renderer mode + */ + public TTableCellRendererText(CellRendererMode mode) { + this(mode, false); + } + + /** + * Create a new renderer of the given mode. + * + * @param mode + * the renderer mode, cannot be NULL + */ + public TTableCellRendererText(CellRendererMode mode, + boolean rightAlign) { + super(mode); + + this.rightAlign = rightAlign; + } + + @Override + public void renderTableCell(TTable table, Object value, int rowIndex, + int colIndex, int y) { + + int xOffset = getXOffset(table, colIndex); + TTableColumn tcol = table.getColumns().get(colIndex); + String data = asText(value, tcol.getWidth(), rightAlign); + + if (!data.isEmpty()) { + boolean isSelected = table.getSelectedRow() == rowIndex; + boolean hasFocus = table.isAbsoluteActive(); + CellAttributes color = getCellAttributes(table.getWindow() + .getApplication().getTheme(), isSelected, hasFocus); + table.getScreen().putStringXY(xOffset, y, data, color); + } + } +} diff --git a/src/be/nikiroo/jexer/TTableCellRendererWidget.java b/src/be/nikiroo/jexer/TTableCellRendererWidget.java new file mode 100644 index 0000000..22c6f47 --- /dev/null +++ b/src/be/nikiroo/jexer/TTableCellRendererWidget.java @@ -0,0 +1,170 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2019 David "Niki" ROULET + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author David ROULET [niki@nikiroo.be] + * @version 1 + */ +package be.nikiroo.jexer; + +import java.util.HashMap; +import java.util.Map; + +import jexer.TLabel; +import jexer.TWidget; + +/** + * A simple {@link TTableCellRenderer} that display the values within a + * {@link TLabel}. + *

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

+ * Will just update the sizes in this case. + */ + public void reflowData() { + if (model != null) { + int maxDataSize = 0; + for (int i = 0; i < model.getRowCount(); i++) { + maxDataSize = Math.max( + maxDataSize, + getRenderer().getWidthOf( + model.getValueAt(i, modelIndex))); + } + + autoWidth = maxDataSize; + if (!forcedWidth) { + setWidth(maxDataSize); + } + } else { + autoWidth = 0; + forcedWidth = false; + width = 0; + } + } + + public int getModelIndex() { + return modelIndex; + } + + /** + * The actual size of the column. This can be auto-computed in some cases. + * + * @return the width (never < 0) + */ + public int getWidth() { + return width; + } + + /** + * Set the actual size of the column or -1 for auto size. + * + * @param width + * the width (or -1 for auto) + */ + public void setWidth(int width) { + forcedWidth = width >= 0; + + if (forcedWidth) { + this.width = width; + } else { + this.width = autoWidth; + } + } + + /** + * The width was forced by the user (using + * {@link TTableColumn#setWidth(int)} with a positive value). + * + * @return TRUE if it was + */ + public boolean isForcedWidth() { + return forcedWidth; + } + + // not an actual forced width, but does change the width return + void expandWidthTo(int width) { + this.width = width; + } + + public Object getHeaderValue() { + return headerValue; + } + + public void setHeaderValue(Object headerValue) { + this.headerValue = headerValue; + } +} diff --git a/src/be/nikiroo/jexer/TTableLine.java b/src/be/nikiroo/jexer/TTableLine.java new file mode 100644 index 0000000..f393621 --- /dev/null +++ b/src/be/nikiroo/jexer/TTableLine.java @@ -0,0 +1,135 @@ +package be.nikiroo.jexer; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +public class TTableLine implements List { + //TODO: in TTable: default to header of size 1 + private List list; + + public TTableLine(List list) { + this.list = list; + } + + // TODO: override this and the rest shall follow + protected List getList() { + return list; + } + + @Override + public int size() { + return getList().size(); + } + + @Override + public boolean isEmpty() { + return getList().isEmpty(); + } + + @Override + public boolean contains(Object o) { + return getList().contains(o); + } + + @Override + public Iterator iterator() { + return getList().iterator(); + } + + @Override + public Object[] toArray() { + return getList().toArray(); + } + + @Override + public T[] toArray(T[] a) { + return getList().toArray(a); + } + + @Override + public boolean containsAll(Collection c) { + return getList().containsAll(c); + } + + @Override + public String get(int index) { + return getList().get(index); + } + + @Override + public int indexOf(Object o) { + return getList().indexOf(o); + } + + @Override + public int lastIndexOf(Object o) { + return getList().lastIndexOf(o); + } + + @Override + public List subList(int fromIndex, int toIndex) { + return getList().subList(fromIndex, toIndex); + } + + @Override + public ListIterator listIterator() { + return getList().listIterator(); + } + + @Override + public ListIterator listIterator(int index) { + return getList().listIterator(index); + } + + @Override + public boolean add(String e) { + throw new UnsupportedOperationException("Read-only collection"); + } + + @Override + public boolean remove(Object o) { + throw new UnsupportedOperationException("Read-only collection"); + } + + @Override + public boolean addAll(Collection c) { + throw new UnsupportedOperationException("Read-only collection"); + } + + @Override + public boolean addAll(int index, Collection c) { + throw new UnsupportedOperationException("Read-only collection"); + } + + @Override + public boolean removeAll(Collection c) { + throw new UnsupportedOperationException("Read-only collection"); + } + + @Override + public boolean retainAll(Collection c) { + throw new UnsupportedOperationException("Read-only collection"); + } + + @Override + public void clear() { + throw new UnsupportedOperationException("Read-only collection"); + } + + @Override + public String set(int index, String element) { + throw new UnsupportedOperationException("Read-only collection"); + } + + @Override + public void add(int index, String element) { + throw new UnsupportedOperationException("Read-only collection"); + } + + @Override + public String remove(int index) { + throw new UnsupportedOperationException("Read-only collection"); + } +} diff --git a/src/be/nikiroo/jexer/TTableModel.java b/src/be/nikiroo/jexer/TTableModel.java new file mode 100644 index 0000000..cd86d35 --- /dev/null +++ b/src/be/nikiroo/jexer/TTableModel.java @@ -0,0 +1,176 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * Copyright (C) 2019 David "Niki" ROULET + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author David ROULET [niki@nikiroo.be] + * @version 1 + */ +package be.nikiroo.jexer; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; + +import javax.swing.event.TableModelListener; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.TableModel; + +/** + * The model of a {@link TTable}. It contains the data of the table and allows + * you access to it. + *

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

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