From 28ba1514344ddc27a6e4222297ab03da849e453c Mon Sep 17 00:00:00 2001 From: Niki Roo Date: Tue, 5 May 2020 13:44:15 +0200 Subject: [PATCH] missing pieces from old fanfix --- src/be/nikiroo/fanfix_jexer/Main.java | 8 +- .../fanfix_jexer/reader/ConfigItem.java | 362 ++++++++++++++ .../fanfix_jexer/reader/ConfigItemString.java | 50 ++ .../fanfix_jexer/reader/TOptionWindow.java | 120 +++++ .../reader/TSimpleScrollableWindow.java | 150 ++++++ .../fanfix_jexer/reader/TuiReader.java | 107 ++++ .../reader/TuiReaderApplication.java | 467 ++++++++++++++++++ .../reader/TuiReaderMainWindow.java | 374 ++++++++++++++ .../reader/TuiReaderOptionWindow.java | 15 + .../reader/TuiReaderStoryWindow.java | 305 ++++++++++++ 10 files changed, 1952 insertions(+), 6 deletions(-) create mode 100644 src/be/nikiroo/fanfix_jexer/reader/ConfigItem.java create mode 100644 src/be/nikiroo/fanfix_jexer/reader/ConfigItemString.java create mode 100644 src/be/nikiroo/fanfix_jexer/reader/TOptionWindow.java create mode 100644 src/be/nikiroo/fanfix_jexer/reader/TSimpleScrollableWindow.java create mode 100644 src/be/nikiroo/fanfix_jexer/reader/TuiReader.java create mode 100644 src/be/nikiroo/fanfix_jexer/reader/TuiReaderApplication.java create mode 100644 src/be/nikiroo/fanfix_jexer/reader/TuiReaderMainWindow.java create mode 100644 src/be/nikiroo/fanfix_jexer/reader/TuiReaderOptionWindow.java create mode 100644 src/be/nikiroo/fanfix_jexer/reader/TuiReaderStoryWindow.java diff --git a/src/be/nikiroo/fanfix_jexer/Main.java b/src/be/nikiroo/fanfix_jexer/Main.java index eba2d2e..f5c53a3 100644 --- a/src/be/nikiroo/fanfix_jexer/Main.java +++ b/src/be/nikiroo/fanfix_jexer/Main.java @@ -4,7 +4,7 @@ import java.io.IOException; import be.nikiroo.fanfix.Instance; import be.nikiroo.fanfix.reader.Reader.ReaderType; -import be.nikiroo.fanfix.reader.tui.TuiReader; +import be.nikiroo.fanfix_jexer.reader.TuiReader; /** * The main class of the application, the launcher. @@ -34,10 +34,6 @@ public class Main { Instance.init(); TuiReader.setDefaultReaderType(ReaderType.TUI); - try { - TuiReader.getReader().browse(null); - } catch (IOException e) { - Instance.getInstance().getTraceHandler().error(e); - } + new TuiReader().browse(null); } } diff --git a/src/be/nikiroo/fanfix_jexer/reader/ConfigItem.java b/src/be/nikiroo/fanfix_jexer/reader/ConfigItem.java new file mode 100644 index 0000000..d1bd93a --- /dev/null +++ b/src/be/nikiroo/fanfix_jexer/reader/ConfigItem.java @@ -0,0 +1,362 @@ +package be.nikiroo.fanfix_jexer.reader; + +import java.util.List; + +import jexer.TAction; +import jexer.TButton; +import jexer.TLabel; +import jexer.TPanel; +import jexer.TWidget; +import be.nikiroo.utils.resources.Bundle; +import be.nikiroo.utils.resources.MetaInfo; +import be.nikiroo.utils.ui.ConfigItemBase; + +/** + * A graphical item that reflect a configuration option from the given + * {@link Bundle}. + *

+ * This graphical item can be edited, and the result will be saved back into the + * linked {@link MetaInfo}; you still have to save the {@link MetaInfo} should + * you wish to, of course. + * + * @author niki + * + * @param + * the type of {@link Bundle} to edit + */ +public abstract class ConfigItem> extends TWidget { + /** The code base */ + private final ConfigItemBase base; + + /** + * Prepare a new {@link ConfigItem} instance, linked to the given + * {@link MetaInfo}. + * + * @param parent + * the parent widget + * @param info + * the info + * @param autoDirtyHandling + * TRUE to automatically manage the setDirty/Save operations, + * FALSE if you want to do it yourself via + * {@link ConfigItem#setDirtyItem(int)} + */ + protected ConfigItem(TWidget parent, MetaInfo info, + boolean autoDirtyHandling) { + super(parent); + + base = new ConfigItemBase(info, autoDirtyHandling) { + @Override + protected TWidget createEmptyField(int item) { + return ConfigItem.this.createEmptyField(item); + } + + @Override + protected Object getFromInfo(int item) { + return ConfigItem.this.getFromInfo(item); + } + + @Override + protected void setToInfo(Object value, int item) { + ConfigItem.this.setToInfo(value, item); + } + + @Override + protected Object getFromField(int item) { + return ConfigItem.this.getFromField(item); + } + + @Override + protected void setToField(Object value, int item) { + ConfigItem.this.setToField(value, item); + } + + @Override + public TWidget createField(int item) { + TWidget field = super.createField(item); + + // TODO: size? + + return field; + } + + @Override + public List reload() { + List removed = base.reload(); + if (!removed.isEmpty()) { + for (TWidget c : removed) { + removeChild(c); + } + } + + return removed; + } + }; + } + + /** + * Create a new {@link ConfigItem} for the given {@link MetaInfo}. + * + * @param nhgap + * negative horisontal gap in pixel to use for the label, i.e., + * the step lock sized labels will start smaller by that amount + * (the use case would be to align controls that start at a + * different horisontal position) + */ + public void init(int nhgap) { + if (getInfo().isArray()) { + // TODO: width + int size = getInfo().getListSize(false); + final TPanel pane = new TPanel(this, 0, 0, 20, size + 2); + final TWidget label = label(0, 0, nhgap); + label.setParent(pane, false); + setHeight(pane.getHeight()); + + for (int i = 0; i < size; i++) { + // TODO: minusPanel + TWidget field = base.addItem(i, null); + field.setParent(pane, false); + field.setX(label.getWidth() + 1); + field.setY(i); + } + + // x, y + final TButton add = new TButton(pane, "+", label.getWidth() + 1, + size + 1, null); + TAction action = new TAction() { + @Override + public void DO() { + TWidget field = base.addItem(base.getFieldsSize(), null); + field.setParent(pane, false); + field.setX(label.getWidth() + 1); + field.setY(add.getY()); + add.setY(add.getY() + 1); + } + }; + add.setAction(action); + } else { + final TWidget label = label(0, 0, nhgap); + + TWidget field = base.createField(-1); + field.setX(label.getWidth() + 1); + field.setWidth(10); // TODO + + // TODO + setWidth(30); + setHeight(1); + } + } + + /** The {@link MetaInfo} linked to the field. */ + public MetaInfo getInfo() { + return base.getInfo(); + } + + /** + * Retrieve the associated graphical component that was created with + * {@link ConfigItemBase#createEmptyField(int)}. + * + * @param item + * the item number to get for an array of values, or -1 to get + * the whole value (has no effect if {@link MetaInfo#isArray()} + * is FALSE) + * + * @return the graphical component + */ + protected TWidget getField(int item) { + return base.getField(item); + } + + /** + * Manually specify that the given item is "dirty" and thus should be saved + * when asked. + *

+ * Has no effect if the class is using automatic dirty handling (see + * {@link ConfigItemBase#ConfigItem(MetaInfo, boolean)}). + * + * @param item + * the item number to get for an array of values, or -1 to get + * the whole value (has no effect if {@link MetaInfo#isArray()} + * is FALSE) + */ + protected void setDirtyItem(int item) { + base.setDirtyItem(item); + } + + /** + * Check if the value changed since the last load/save into the linked + * {@link MetaInfo}. + *

+ * Note that we consider NULL and an Empty {@link String} to be equals. + * + * @param value + * the value to test + * @param item + * the item number to get for an array of values, or -1 to get + * the whole value (has no effect if {@link MetaInfo#isArray()} + * is FALSE) + * + * @return TRUE if it has + */ + protected boolean hasValueChanged(Object value, int item) { + return base.hasValueChanged(value, item); + } + + /** + * Create an empty graphical component to be used later by + * {@link ConfigItem#createField(int)}. + *

+ * Note that {@link ConfigItem#reload(int)} will be called after it was + * created by {@link ConfigItem#createField(int)}. + * + * @param item + * the item number to get for an array of values, or -1 to get + * the whole value (has no effect if {@link MetaInfo#isArray()} + * is FALSE) + * + * @return the graphical component + */ + abstract protected TWidget createEmptyField(int item); + + /** + * Get the information from the {@link MetaInfo} in the subclass preferred + * format. + * + * @param item + * the item number to get for an array of values, or -1 to get + * the whole value (has no effect if {@link MetaInfo#isArray()} + * is FALSE) + * + * @return the information in the subclass preferred format + */ + abstract protected Object getFromInfo(int item); + + /** + * Set the value to the {@link MetaInfo}. + * + * @param value + * the value in the subclass preferred format + * @param item + * the item number to get for an array of values, or -1 to get + * the whole value (has no effect if {@link MetaInfo#isArray()} + * is FALSE) + */ + abstract protected void setToInfo(Object value, int item); + + /** + * The value present in the given item's related field in the subclass + * preferred format. + * + * @param item + * the item number to get for an array of values, or -1 to get + * the whole value (has no effect if {@link MetaInfo#isArray()} + * is FALSE) + * + * @return the value present in the given item's related field in the + * subclass preferred format + */ + abstract protected Object getFromField(int item); + + /** + * Set the value (in the subclass preferred format) into the field. + * + * @param value + * the value in the subclass preferred format + * @param item + * the item number to get for an array of values, or -1 to get + * the whole value (has no effect if {@link MetaInfo#isArray()} + * is FALSE) + */ + abstract protected void setToField(Object value, int item); + + /** + * Create a label which width is constrained in lock steps. + * + * @param x + * the X position of the label + * @param y + * the Y position of the label + * @param nhgap + * negative horisontal gap in pixel to use for the label, i.e., + * the step lock sized labels will start smaller by that amount + * (the use case would be to align controls that start at a + * different horisontal position) + * + * @return the label + */ + protected TWidget label(int x, int y, int nhgap) { + // TODO: see Swing version for lock-step sizes + // TODO: see Swing version for help info-buttons + + String lbl = getInfo().getName(); + return new TLabel(this, lbl, x, y); + } + + /** + * Create a new {@link ConfigItem} for the given {@link MetaInfo}. + * + * @param + * the type of {@link Bundle} to edit + * + * @param x + * the X position of the item + * @param y + * the Y position of the item + * @param parent + * the parent widget to use for this one + * @param info + * the {@link MetaInfo} + * @param nhgap + * negative horisontal gap in pixel to use for the label, i.e., + * the step lock sized labels will start smaller by that amount + * (the use case would be to align controls that start at a + * different horisontal position) + * + * @return the new {@link ConfigItem} + */ + static public > ConfigItem createItem(TWidget parent, + int x, int y, MetaInfo info, int nhgap) { + + ConfigItem configItem; + switch (info.getFormat()) { + // TODO + // case BOOLEAN: + // configItem = new ConfigItemBoolean(info); + // break; + // case COLOR: + // configItem = new ConfigItemColor(info); + // break; + // case FILE: + // configItem = new ConfigItemBrowse(info, false); + // break; + // case DIRECTORY: + // configItem = new ConfigItemBrowse(info, true); + // break; + // case COMBO_LIST: + // configItem = new ConfigItemCombobox(info, true); + // break; + // case FIXED_LIST: + // configItem = new ConfigItemCombobox(info, false); + // break; + // case INT: + // configItem = new ConfigItemInteger(info); + // break; + // case PASSWORD: + // configItem = new ConfigItemPassword(info); + // break; + // case LOCALE: + // configItem = new ConfigItemLocale(info); + // break; + // case STRING: + default: + configItem = new ConfigItemString(parent, info); + break; + } + + configItem.init(nhgap); + configItem.setX(x); + configItem.setY(y); + + return configItem; + } +} diff --git a/src/be/nikiroo/fanfix_jexer/reader/ConfigItemString.java b/src/be/nikiroo/fanfix_jexer/reader/ConfigItemString.java new file mode 100644 index 0000000..8403fef --- /dev/null +++ b/src/be/nikiroo/fanfix_jexer/reader/ConfigItemString.java @@ -0,0 +1,50 @@ +package be.nikiroo.fanfix_jexer.reader; + +import jexer.TField; +import jexer.TWidget; +import be.nikiroo.utils.resources.MetaInfo; + +class ConfigItemString> extends ConfigItem { + /** + * Create a new {@link ConfigItemString} for the given {@link MetaInfo}. + * + * @param info + * the {@link MetaInfo} + */ + public ConfigItemString(TWidget parent, MetaInfo info) { + super(parent, info, true); + } + + @Override + protected Object getFromField(int item) { + TField field = (TField) getField(item); + if (field != null) { + return field.getText(); + } + + return null; + } + + @Override + protected Object getFromInfo(int item) { + return getInfo().getString(item, false); + } + + @Override + protected void setToField(Object value, int item) { + TField field = (TField) getField(item); + if (field != null) { + field.setText(value == null ? "" : value.toString()); + } + } + + @Override + protected void setToInfo(Object value, int item) { + getInfo().setString((String) value, item); + } + + @Override + protected TWidget createEmptyField(int item) { + return new TField(this, 0, 0, 1, false); + } +} diff --git a/src/be/nikiroo/fanfix_jexer/reader/TOptionWindow.java b/src/be/nikiroo/fanfix_jexer/reader/TOptionWindow.java new file mode 100644 index 0000000..510d694 --- /dev/null +++ b/src/be/nikiroo/fanfix_jexer/reader/TOptionWindow.java @@ -0,0 +1,120 @@ +package be.nikiroo.fanfix_jexer.reader; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import jexer.TAction; +import jexer.TApplication; +import jexer.TPanel; +import jexer.TWidget; +import jexer.event.TCommandEvent; +import be.nikiroo.utils.StringUtils; +import be.nikiroo.utils.resources.Bundle; +import be.nikiroo.utils.resources.MetaInfo; + +public class TOptionWindow> extends TSimpleScrollableWindow { + private List> items; + + public TOptionWindow(TApplication parent, Class type, + final Bundle bundle, String title) { + super(parent, title, 0, 0, CENTERED | RESIZABLE); + + getMainPane().addLabel(title, 0, 0); + + items = new ArrayList>(); + List> groupedItems = MetaInfo.getItems(type, bundle); + int y = 2; + for (MetaInfo item : groupedItems) { + // will populate this.items + y += addItem(getMainPane(), 5, y, item, 0).getHeight(); + } + + y++; + + setRealHeight(y + 1); + + getMainPane().addButton("Reset", 25, y, new TAction() { + @Override + public void DO() { + for (MetaInfo item : items) { + item.reload(); + } + } + }); + + getMainPane().addButton("Default", 15, y, new TAction() { + @Override + public void DO() { + Object snap = bundle.takeSnapshot(); + bundle.reload(true); + for (MetaInfo item : items) { + item.reload(); + } + bundle.reload(false); + bundle.restoreSnapshot(snap); + } + }); + + getMainPane().addButton("Save", 1, y, new TAction() { + @Override + public void DO() { + for (MetaInfo item : items) { + item.save(true); + } + + try { + bundle.updateFile(); + } catch (IOException e1) { + e1.printStackTrace(); + } + } + }); + } + + private TWidget addItem(TWidget parent, int x, int y, MetaInfo item, + int nhgap) { + if (item.isGroup()) { + // TODO: width + int w = 80 - x; + + String name = item.getName(); + String info = item.getDescription(); + info = StringUtils.justifyTexts(info, w - 3); // -3 for borders + + final TPanel pane = new TPanel(parent, x, y, w, 1); + pane.addLabel(name, 0, 0); + + int h = 0; + if (!info.isEmpty()) { + h += info.split("\n").length + 1; // +1 for scroll + pane.addText(info + "\n", 0, 1, w, h); + } + + // +1 for the title + h++; + + int paneY = h; // for the info desc + for (MetaInfo subitem : item) { + paneY += addItem(pane, 4, paneY, subitem, nhgap + 11) + .getHeight(); + } + + pane.setHeight(paneY); + return pane; + } + + items.add(item); + return ConfigItem.createItem(parent, x, y, item, nhgap); + } + + @Override + public void onCommand(TCommandEvent command) { + if (command.getCmd().equals(TuiReaderApplication.CMD_EXIT)) { + TuiReaderApplication.close(this); + } else { + // Handle our own event if needed here + super.onCommand(command); + } + } +} diff --git a/src/be/nikiroo/fanfix_jexer/reader/TSimpleScrollableWindow.java b/src/be/nikiroo/fanfix_jexer/reader/TSimpleScrollableWindow.java new file mode 100644 index 0000000..e795656 --- /dev/null +++ b/src/be/nikiroo/fanfix_jexer/reader/TSimpleScrollableWindow.java @@ -0,0 +1,150 @@ +package be.nikiroo.fanfix_jexer.reader; + +import jexer.TApplication; +import jexer.THScroller; +import jexer.TPanel; +import jexer.TScrollableWindow; +import jexer.TVScroller; +import jexer.TWidget; +import jexer.event.TMouseEvent; +import jexer.event.TResizeEvent; + +public class TSimpleScrollableWindow extends TScrollableWindow { + protected TPanel mainPane; + private int prevHorizontal = -1; + private int prevVertical = -1; + + public TSimpleScrollableWindow(TApplication application, String title, + int width, int height) { + this(application, title, width, height, 0, 0, 0); + } + + public TSimpleScrollableWindow(TApplication application, String title, + int width, int height, int flags) { + this(application, title, width, height, flags, 0, 0); + } + + // 0 = none (so, no scrollbar) + public TSimpleScrollableWindow(TApplication application, String title, + int width, int height, int flags, int realWidth, int realHeight) { + super(application, title, width, height, flags); + + mainPane = new TPanel(this, 0, 0, 1, 1) { + @Override + public void draw() { + for (TWidget children : mainPane.getChildren()) { + int y = children.getY() + children.getHeight(); + int x = children.getX() + children.getWidth(); + boolean visible = (y > getVerticalValue()) + && (x > getHorizontalValue()); + children.setVisible(visible); + } + super.draw(); + } + }; + + mainPane.setWidth(getWidth()); + mainPane.setHeight(getHeight()); + + setRealWidth(realWidth); + setRealHeight(realHeight); + placeScrollbars(); + } + + /** + * The main pane on which you can add other widgets for this scrollable + * window. + * + * @return the main pane + */ + public TPanel getMainPane() { + return mainPane; + } + + public void setRealWidth(int realWidth) { + if (realWidth <= 0) { + if (hScroller != null) { + hScroller.remove(); + } + } else { + if (hScroller == null) { + // size/position will be fixed by placeScrollbars() + hScroller = new THScroller(this, 0, 0, 10); + } + setRightValue(realWidth); + } + + reflowData(); + } + + public void setRealHeight(int realHeight) { + if (realHeight <= 0) { + if (vScroller != null) { + vScroller.remove(); + } + } else { + if (vScroller == null) { + // size/position will be fixed by placeScrollbars() + vScroller = new TVScroller(this, 0, 0, 10); + } + setBottomValue(realHeight); + } + + reflowData(); + } + + @Override + public void onResize(TResizeEvent event) { + super.onResize(event); + mainPane.setWidth(getWidth()); + mainPane.setHeight(getHeight()); + mainPane.onResize(event); + } + + @Override + public void reflowData() { + super.reflowData(); + reflowData(getHorizontalValue(), getVerticalValue()); + } + + protected void reflowData(int totalX, int totalY) { + super.reflowData(); + mainPane.setX(-totalX); + mainPane.setY(-totalY); + } + + @Override + public void onMouseUp(TMouseEvent mouse) { + super.onMouseUp(mouse); + + // TODO: why? this should already be done by the scrollers + // it could also mean we do it twice if, somehow, it sometime works... + int mrx = mouse.getX(); + int mry = mouse.getY(); + + int mx = mouse.getAbsoluteX(); + int my = mouse.getAbsoluteY(); + + if (vScroller != null) { + mouse.setX(mx - vScroller.getAbsoluteX()); + mouse.setY(my - vScroller.getAbsoluteY()); + vScroller.onMouseUp(mouse); + } + if (hScroller != null) { + mouse.setX(mx - hScroller.getAbsoluteX()); + mouse.setY(my - hScroller.getAbsoluteY()); + hScroller.onMouseUp(mouse); + } + + mouse.setX(mrx); + mouse.setY(mry); + // + + if (prevHorizontal != getHorizontalValue() + || prevVertical != getVerticalValue()) { + prevHorizontal = getHorizontalValue(); + prevVertical = getVerticalValue(); + reflowData(); + } + } +} diff --git a/src/be/nikiroo/fanfix_jexer/reader/TuiReader.java b/src/be/nikiroo/fanfix_jexer/reader/TuiReader.java new file mode 100644 index 0000000..7eb0f25 --- /dev/null +++ b/src/be/nikiroo/fanfix_jexer/reader/TuiReader.java @@ -0,0 +1,107 @@ +package be.nikiroo.fanfix_jexer.reader; + +import java.io.IOException; + +import jexer.TApplication; +import jexer.TApplication.BackendType; +import be.nikiroo.fanfix.Instance; +import be.nikiroo.fanfix.reader.BasicReader; +import be.nikiroo.fanfix.reader.Reader; +import be.nikiroo.fanfix.supported.SupportType; + +/** + * This {@link Reader}is based upon the TUI widget library 'jexer' + * (https://github.com/klamonte/jexer/) and offer, as its name suggest, a Text + * User Interface. + *

+ * It is expected to be on par with the GUI version. + * + * @author niki + */ +public class TuiReader extends BasicReader { + /** + * Will detect the backend to use. + *

+ * Swing is the default backend on Windows and MacOS while evreything else + * will use XTERM unless explicitly overridden by jexer.Swing = + * true or false. + * + * @return the backend to use + */ + private static BackendType guessBackendType() { + // TODO: allow a config option to force one or the other? + TApplication.BackendType backendType = TApplication.BackendType.XTERM; + if (System.getProperty("os.name").startsWith("Windows")) { + backendType = TApplication.BackendType.SWING; + } + + if (System.getProperty("os.name").startsWith("Mac")) { + backendType = TApplication.BackendType.SWING; + } + + if (System.getProperty("jexer.Swing") != null) { + if (System.getProperty("jexer.Swing", "false").equals("true")) { + backendType = TApplication.BackendType.SWING; + } else { + backendType = TApplication.BackendType.XTERM; + } + } + + return backendType; + } + + @Override + public void read(boolean sync) throws IOException { + // TODO + if (!sync) { + // How could you do a not-sync in TUI mode? + throw new java.lang.IllegalStateException( + "Async mode not implemented yet."); + } + + try { + TuiReaderApplication app = new TuiReaderApplication(this, + guessBackendType()); + app.run(); + } catch (Exception e) { + Instance.getInstance().getTraceHandler().error(e); + } + } + + @Override + public void browse(String source) { + try { + TuiReaderApplication app = new TuiReaderApplication(this, source, + guessBackendType()); + app.run(); + } catch (Exception e) { + Instance.getInstance().getTraceHandler().error(e); + } + } + + @Override + public void search(boolean sync) throws IOException { + // TODO + if (sync) { + throw new java.lang.IllegalStateException("Not implemented yet."); + } + } + + @Override + public void search(SupportType searchOn, String keywords, int page, + int item, boolean sync) { + // TODO + if (sync) { + throw new java.lang.IllegalStateException("Not implemented yet."); + } + } + + @Override + public void searchTag(SupportType searchOn, int page, int item, + boolean sync, Integer... tags) { + // TODO + if (sync) { + throw new java.lang.IllegalStateException("Not implemented yet."); + } + } +} diff --git a/src/be/nikiroo/fanfix_jexer/reader/TuiReaderApplication.java b/src/be/nikiroo/fanfix_jexer/reader/TuiReaderApplication.java new file mode 100644 index 0000000..f621309 --- /dev/null +++ b/src/be/nikiroo/fanfix_jexer/reader/TuiReaderApplication.java @@ -0,0 +1,467 @@ +package be.nikiroo.fanfix_jexer.reader; + +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; +import jexer.TKeypress; +import jexer.TMessageBox; +import jexer.TMessageBox.Result; +import jexer.TMessageBox.Type; +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; +import be.nikiroo.fanfix.data.MetaData; +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.supported.SupportType; +import be.nikiroo.fanfix_jexer.reader.TuiReaderMainWindow.Mode; +import be.nikiroo.utils.Progress; + +/** + * Manages the TUI general mode and links and manages the {@link TWindow}s. + *

+ * It will also enclose a {@link Reader} and simply handle the reading part + * differently (it will create the required sub-windows and display them). + * + * @author niki + */ +class TuiReaderApplication extends TApplication implements Reader { + public static final int MENU_FILE_OPEN = 1025; + public static final int MENU_FILE_IMPORT_URL = 1026; + public static final int MENU_FILE_IMPORT_FILE = 1027; + public static final int MENU_FILE_EXPORT = 1028; + public static final int MENU_FILE_DELETE = 1029; + public static final int MENU_FILE_LIBRARY = 1030; + public static final int MENU_FILE_EXIT = 1031; + // + public static final int MENU_OPT_FANFIX = 1032; + public static final int MENU_OPT_TUI = 1033; + + + public static final TCommand CMD_EXIT = new TCommand(MENU_FILE_EXIT) { + }; + + private Reader reader; + private TuiReaderMainWindow main; + + // start reading if meta present + public TuiReaderApplication(Reader reader, BackendType backend) + throws Exception { + super(backend); + init(reader); + + if (getMeta() != null) { + read(false); + } + } + + public TuiReaderApplication(Reader reader, String source, + TApplication.BackendType backend) throws Exception { + super(backend); + init(reader); + + showMain(); + main.setMode(Mode.SOURCE, source); + } + + @Override + public void read(boolean sync) throws IOException { + read(getStory(null), sync); + } + + @Override + public MetaData getMeta() { + return reader.getMeta(); + } + + @Override + public Story getStory(Progress pg) throws IOException { + return reader.getStory(pg); + } + + @Override + public BasicLibrary getLibrary() { + return reader.getLibrary(); + } + + @Override + public void setLibrary(BasicLibrary lib) { + reader.setLibrary(lib); + } + + @Override + public void setMeta(MetaData meta) throws IOException { + reader.setMeta(meta); + } + + @Override + public void setMeta(String luid) throws IOException { + reader.setMeta(luid); + } + + @Override + public void setMeta(URL source, Progress pg) throws IOException { + reader.setMeta(source, pg); + } + + @Override + public void browse(String source) { + try { + reader.browse(source); + } catch (IOException e) { + Instance.getInstance().getTraceHandler().error(e); + } + } + + @Override + public int getChapter() { + return reader.getChapter(); + } + + @Override + public void setChapter(int chapter) { + reader.setChapter(chapter); + } + + @Override + public void search(boolean sync) throws IOException { + reader.search(sync); + } + + @Override + public void search(SupportType searchOn, String keywords, int page, + int item, boolean sync) throws IOException { + reader.search(searchOn, keywords, page, item, sync); + } + + @Override + public void searchTag(SupportType searchOn, int page, int item, + boolean sync, Integer... tags) throws IOException { + reader.searchTag(searchOn, page, item, sync, tags); + } + + /** + * Open the given {@link Story} for reading. This may or may not start an + * external program to read said {@link Story}. + * + * @param story + * the {@link Story} to read + * @param sync + * execute the process synchronously (wait until it is terminated + * before returning) + * + * @throws IOException + * in case of I/O errors + */ + public void read(Story story, boolean sync) 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(), sync); + } 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. + *

+ * Some shortcuts are always visible, and will be put here. + *

+ * Note that shortcuts placed this way on menu won't work unless the menu + * also implement them. + * + * @param window + * the new window or menu on screen + * @param description + * the description to show on the status ba + */ + public TStatusBar setStatusBar(TWindow window, String description) { + TStatusBar statusBar = window.newStatusBar(description); + statusBar.addShortcutKeypress(TKeypress.kbF10, CMD_EXIT, "Exit"); + return statusBar; + + } + + private void showMain() { + if (main != null && main.isVisible()) { + main.activate(); + } else { + if (main != null) { + main.close(); + } + main = new TuiReaderMainWindow(this); + main.maximize(); + } + } + + private void init(Reader reader) { + this.reader = reader; + + // TODO: traces/errors? + Instance.getInstance().setTraceHandler(null); + + // Add the menus TODO: i18n + TMenu fileMenu = addMenu("&File"); + fileMenu.addItem(MENU_FILE_OPEN, "&Open..."); + fileMenu.addItem(MENU_FILE_EXPORT, "&Save as..."); + fileMenu.addItem(MENU_FILE_DELETE, "&Delete..."); + // TODO: Move to... + fileMenu.addSeparator(); + fileMenu.addItem(MENU_FILE_IMPORT_URL, "Import &URL..."); + fileMenu.addItem(MENU_FILE_IMPORT_FILE, "Import &file..."); + fileMenu.addSeparator(); + fileMenu.addItem(MENU_FILE_LIBRARY, "Lib&rary"); + fileMenu.addSeparator(); + fileMenu.addItem(MENU_FILE_EXIT, "E&xit"); + + TMenu OptionsMenu = addMenu("&Options"); + OptionsMenu.addItem(MENU_OPT_FANFIX, "&Fanfix Configuration"); + OptionsMenu.addItem(MENU_OPT_TUI, "&UI Configuration"); + + setStatusBar(fileMenu, "File-management " + + "commands (Open, Save, Print, etc.)"); + + + // TODO: Edit: re-download, delete + + // + + addWindowMenu(); + + 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_FILE_EXIT: + close(this); + return true; + case MENU_FILE_OPEN: + String openfile = null; + try { + openfile = fileOpenBox("."); + reader.setMeta(BasicReader.getUrl(openfile), null); + read(false); + } catch (IOException e) { + // TODO: i18n + error("Fail to open file" + + (openfile == null ? "" : ": " + openfile), + "Import error", e); + } + + return true; + case MENU_FILE_DELETE: + String luid = null; + String story = null; + MetaData meta = null; + if (main != null) { + meta = main.getSelectedMeta(); + } + if (meta != null) { + luid = meta.getLuid(); + story = luid + ": " + meta.getTitle(); + } + + // TODO: i18n + TMessageBox mbox = messageBox("Delete story", "Delete story \"" + + story + "\"", Type.OKCANCEL); + if (mbox.getResult() == Result.OK) { + try { + reader.getLibrary().delete(luid); + if (main != null) { + main.refreshStories(); + } + } catch (IOException e) { + // TODO: i18n + error("Fail to delete the story: \"" + story + "\"", + "Error", e); + } + } + + return true; + case MENU_FILE_IMPORT_URL: + String clipboard = ""; + try { + clipboard = ("" + Toolkit.getDefaultToolkit() + .getSystemClipboard().getData(DataFlavor.stringFlavor)) + .trim(); + } catch (Exception e) { + // No data will be handled + } + + if (clipboard == null || !clipboard.startsWith("http")) { + clipboard = ""; + } + + String url = inputBox("Import story", "URL to import", clipboard) + .getText(); + + 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_FILE_IMPORT_FILE: + String filename = null; + try { + filename = fileOpenBox("."); + if (!imprt(filename)) { + // TODO: i18n + error("File not supported: " + filename, "Import error"); + } + } catch (IOException e) { + // TODO: i18n + error("Fail to import file" + + (filename == null ? "" : ": " + filename), + "Import error", e); + } + return true; + case MENU_FILE_LIBRARY: + showMain(); + return true; + + case MENU_OPT_FANFIX: + new TuiReaderOptionWindow(this, false).maximize(); + return true; + + case MENU_OPT_TUI: + new TuiReaderOptionWindow(this, true).maximize(); + return true; + + } + + return super.onMenu(menu); + } + + /** + * 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 (UnknownHostException e) { + return false; + } + } + + @Override + public void openExternal(BasicLibrary lib, String luid, boolean sync) + throws IOException { + reader.openExternal(lib, luid, sync); + } + + /** + * 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.getInstance().getTraceHandler().error(title + ": " + message); + if (e != null) { + Instance.getInstance().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. + *

+ * This should result in the program terminating. + * + * @param widget + * the {@link TWidget} + */ + static public void close(TWidget widget) { + close(widget.getApplication()); + } + + /** + * Ask the user and, if confirmed, close the {@link TApplication}. + *

+ * This should result in the program terminating. + * + * @param app + * the {@link TApplication} + */ + static void close(TApplication app) { + // TODO: i18n + if (app.messageBox("Confirmation", "(TODO: i18n) Exit application?", + TMessageBox.Type.YESNO).getResult() == TMessageBox.Result.YES) { + app.exit(); + } + } +} diff --git a/src/be/nikiroo/fanfix_jexer/reader/TuiReaderMainWindow.java b/src/be/nikiroo/fanfix_jexer/reader/TuiReaderMainWindow.java new file mode 100644 index 0000000..2429583 --- /dev/null +++ b/src/be/nikiroo/fanfix_jexer/reader/TuiReaderMainWindow.java @@ -0,0 +1,374 @@ +package be.nikiroo.fanfix_jexer.reader; + +import java.io.IOException; +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; +import be.nikiroo.fanfix.output.BasicOutput.OutputType; +import be.nikiroo.jexer.TSizeConstraint; + +/** + * The library window, that will list all the (filtered) stories available in + * this {@link BasicLibrary}. + * + * @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 TuiReaderApplication reader; + + 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. + * + * @param reader + * the reader and main application + */ + public TuiReaderMainWindow(TuiReaderApplication reader) { + // Construct a demo window. X and Y don't matter because it will be + // centred on screen. + super(reader, "Library", 0, 0, 60, 18, CENTERED | RESIZABLE); + + this.reader = reader; + + listKeys = new ArrayList(); + listItems = new ArrayList(); + + 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 + + // addLabel("Label (1,1)", 1, 1); + // addButton("&Button (35,1)", 35, 1, new TAction() { + // public void DO() { + // } + // }); + // addCheckbox(1, 2, "Checky (1,2)", false); + // addProgressBar(1, 3, 30, 42); + // TRadioGroup groupy = addRadioGroup(1, 4, "Radio groupy"); + // groupy.addRadioButton("Fanfan"); + // groupy.addRadioButton("Tulipe"); + // addField(1, 10, 20, false, "text not fixed."); + // addField(1, 11, 20, true, "text fixed."); + // addText("20x4 Text in (12,20)", 1, 12, 20, 4); + // + // TTreeView tree = addTreeView(30, 5, 20, 5); + // TTreeItem root = new TTreeItem(tree, "expended root", true); + // tree.setSelected(root); // needed to allow arrow navigation without + // // mouse-clicking before + // + // root.addChild("child"); + // 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)"); + try { + for (String source : reader.getLibrary().getSources()) { + selectTargets.add(source); + } + } catch (IOException e) { + Instance.getInstance().getTraceHandler().error(e); + } + + showTarget = true; + } else { + selectTargets.clear(); + selectTargets.add("(show all)"); + try { + for (String author : reader.getLibrary().getAuthors()) { + selectTargets.add(author); + } + } catch (IOException e) { + Instance.getInstance().getTraceHandler().error(e); + } + + 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); + super.onClose(); + } + + /** + * Refresh the list of stories displayed in this library. + *

+ * Will take the current settings into account (filter, source...). + */ + public void refreshStories() { + List metas; + + try { + if (mode == Mode.SOURCE) { + metas = reader.getLibrary().getList().filter(target, null, null); + } else if (mode == Mode.AUTHOR) { + metas = reader.getLibrary().getList().filter(null, target, null); + } else { + metas = reader.getLibrary().getList().getMetas(); + } + } catch (IOException e) { + Instance.getInstance().getTraceHandler().error(e); + metas = new ArrayList(); + } + + setMetas(metas); + } + + /** + * Change the author/source filter and display all stories matching this + * target. + * + * @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 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 + */ + private void setMetas(List metas) { + listKeys.clear(); + listItems.clear(); + + if (metas != null) { + for (MetaData meta : metas) { + 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() { + if (list.getSelectedIndex() >= 0) { + return listKeys.get(list.getSelectedIndex()); + } + + return null; + } + + public void readStory(MetaData meta) { + try { + reader.setChapter(-1); + reader.setMeta(meta); + reader.read(false); + } catch (IOException e) { + Instance.getInstance().getTraceHandler().error(e); + } + } + + private String desc(MetaData meta) { + return String.format("%5s: %s", meta.getLuid(), meta.getTitle()); + } + + @Override + public void onCommand(TCommandEvent command) { + if (command.getCmd().equals(TuiReaderApplication.CMD_EXIT)) { + TuiReaderApplication.close(this); + } else { + // Handle our own event if needed here + super.onCommand(command); + } + } + + @Override + public void onMenu(TMenuEvent menu) { + MetaData meta = getSelectedMeta(); + if (meta != null) { + switch (menu.getId()) { + case TuiReaderApplication.MENU_FILE_OPEN: + readStory(meta); + + return; + case TuiReaderApplication.MENU_FILE_EXPORT: + + try { + // TODO: choose type, pg, error + OutputType outputType = OutputType.EPUB; + String path = fileOpenBox(".", Type.SAVE); + reader.getLibrary().export(meta.getLuid(), outputType, + path, null); + } catch (IOException e) { + // TODO + e.printStackTrace(); + } + + return; + + case -1: + try { + reader.getLibrary().delete(meta.getLuid()); + } catch (IOException e) { + // TODO + } + + return; + } + } + + super.onMenu(menu); + } +} \ No newline at end of file diff --git a/src/be/nikiroo/fanfix_jexer/reader/TuiReaderOptionWindow.java b/src/be/nikiroo/fanfix_jexer/reader/TuiReaderOptionWindow.java new file mode 100644 index 0000000..a02ad99 --- /dev/null +++ b/src/be/nikiroo/fanfix_jexer/reader/TuiReaderOptionWindow.java @@ -0,0 +1,15 @@ +package be.nikiroo.fanfix_jexer.reader; + +import jexer.TStatusBar; +import be.nikiroo.fanfix.Instance; +import be.nikiroo.fanfix.bundles.Config; +import be.nikiroo.fanfix.bundles.UiConfig; + +class TuiReaderOptionWindow extends TOptionWindow { + public TuiReaderOptionWindow(TuiReaderApplication reader, boolean uiOptions) { + super(reader, uiOptions ? UiConfig.class : Config.class, + uiOptions ? Instance.getInstance().getUiConfig() : Instance.getInstance().getConfig(), "Options"); + + TStatusBar statusBar = reader.setStatusBar(this, "Options"); + } +} diff --git a/src/be/nikiroo/fanfix_jexer/reader/TuiReaderStoryWindow.java b/src/be/nikiroo/fanfix_jexer/reader/TuiReaderStoryWindow.java new file mode 100644 index 0000000..6a6ff7a --- /dev/null +++ b/src/be/nikiroo/fanfix_jexer/reader/TuiReaderStoryWindow.java @@ -0,0 +1,305 @@ +package be.nikiroo.fanfix_jexer.reader; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import jexer.TAction; +import jexer.TButton; +import jexer.TLabel; +import jexer.TText; +import jexer.TWindow; +import jexer.event.TCommandEvent; +import jexer.event.TResizeEvent; +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.reader.BasicReader; +import be.nikiroo.jexer.TSizeConstraint; +import be.nikiroo.jexer.TTable; + +/** + * This window will contain the {@link Story} in a readable format, with a + * chapter browser. + * + * @author niki + */ +class TuiReaderStoryWindow extends TWindow { + private Story story; + private TLabel titleField; + private TText textField; + private TTable table; + 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, Story story, + int chapter) { + super(app, desc(story.getMeta()), 0, 0, 60, 18, CENTERED | RESIZABLE); + + this.story = story; + + app.setStatusBar(this, desc(story.getMeta())); + + // last = use window background + titleField = new TLabel(this, " Title", 0, 1, "tlabel", false); + textField = new TText(this, "", 0, 0, 1, 1); + table = new TTable(this, 0, 0, 1, 1, null, null, Arrays.asList("Key", + "Value"), true); + + titleField.setEnabled(false); + + navigationButtons = new ArrayList(5); + + navigationButtons.add(addButton("<<", 0, 0, new TAction() { + @Override + public void DO() { + setChapter(-1); + } + })); + navigationButtons.add(addButton("< ", 4, 0, new TAction() { + @Override + public void DO() { + setChapter(TuiReaderStoryWindow.this.chapter - 1); + } + })); + navigationButtons.add(addButton("> ", 7, 0, new TAction() { + @Override + public void DO() { + setChapter(TuiReaderStoryWindow.this.chapter + 1); + } + })); + navigationButtons.add(addButton(">>", 10, 0, new TAction() { + @Override + public void DO() { + setChapter(getStory().getChapters().size()); + } + })); + + navigationButtons.get(0).setEnabled(false); + navigationButtons.get(1).setEnabled(false); + + currentChapter = addLabel("", 0, 0); + + TSizeConstraint.setSize(sizeConstraints, textField, 1, 3, -1, -1); + TSizeConstraint.setSize(sizeConstraints, table, 0, 3, 0, -1); + TSizeConstraint.setSize(sizeConstraints, currentChapter, 14, -3, -1, + null); + + for (TButton navigationButton : navigationButtons) { + navigationButton.setShadowColor(null); + // navigationButton.setEmptyBorders(false); + TSizeConstraint.setSize(sizeConstraints, navigationButton, null, + -3, null, null); + } + + onResize(null); + + setChapter(chapter); + } + + @Override + public void onResize(TResizeEvent resize) { + if (resize != null) { + super.onResize(resize); + } + + // TODO: find out why TText and TTable does not behave the same way + // (offset of 2 for height and width) + + TSizeConstraint.resize(sizeConstraints); + + // Improve the disposition of the scrollbars + textField.getVerticalScroller().setX(textField.getWidth()); + textField.getVerticalScroller().setHeight(textField.getHeight()); + textField.getHorizontalScroller().setX(-1); + textField.getHorizontalScroller().setWidth(textField.getWidth() + 1); + + setCurrentChapterText(); + } + + /** + * Display the current chapter in the window, or the {@link Story} info + * page. + * + * @param chapter + * the chapter (including "0" which is the description) or "-1" + * to display the info page instead + */ + private void setChapter(int chapter) { + if (chapter > getStory().getChapters().size()) { + chapter = getStory().getChapters().size(); + } + + if (chapter != this.chapter) { + this.chapter = chapter; + + int max = getStory().getChapters().size(); + navigationButtons.get(0).setEnabled(chapter > -1); + navigationButtons.get(1).setEnabled(chapter > -1); + navigationButtons.get(2).setEnabled(chapter < max); + navigationButtons.get(3).setEnabled(chapter < max); + + if (chapter < 0) { + displayInfoPage(); + } else { + displayChapterPage(); + } + } + + setCurrentChapterText(); + } + + /** + * Append the info page about the current {@link Story}. + * + * @param builder + * the builder to append to + */ + private void displayInfoPage() { + textField.setVisible(false); + table.setVisible(true); + textField.setEnabled(false); + table.setEnabled(true); + + MetaData meta = getStory().getMeta(); + + setCurrentTitle(meta.getTitle()); + + Map metaDesc = BasicReader.getMetaDesc(meta); + String[][] metaDescObj = new String[metaDesc.size()][2]; + int i = 0; + for (String key : metaDesc.keySet()) { + metaDescObj[i][0] = " " + key; + metaDescObj[i][1] = metaDesc.get(key); + i++; + } + + table.setRowData(metaDescObj); + table.setHeaders(Arrays.asList("key", "value"), false); + table.toTop(); + } + + /** + * Append the current chapter. + * + * @param builder + * the builder to append to + */ + private void displayChapterPage() { + table.setVisible(false); + textField.setVisible(true); + table.setEnabled(false); + textField.setEnabled(true); + + StringBuilder builder = new StringBuilder(); + + Chapter chap = null; + if (chapter == 0) { + chap = getStory().getMeta().getResume(); + } else if (chapter > 0) { + chap = getStory().getChapters().get(chapter - 1); + } + + // TODO: i18n + String chapName = chap == null ? "[No RESUME]" : chap.getName(); + setCurrentTitle(String.format("Chapter %d: %s", chapter, chapName)); + + if (chap != null) { + for (Paragraph para : chap) { + if (para.getType() == ParagraphType.BREAK) { + builder.append("\n"); + } + builder.append(para.getContent()).append("\n"); + if (para.getType() == ParagraphType.BREAK) { + builder.append("\n"); + } + } + } + + setText(builder.toString()); + } + + private Story getStory() { + return story; + } + + /** + * Display the given text on the window. + * + * @param text + * the text to display + */ + private void setText(String text) { + textField.setText(text); + textField.reflowData(); + textField.toTop(); + } + + /** + * Set the current chapter area to the correct value. + */ + private void setCurrentChapterText() { + String name; + if (chapter < 0) { + name = " " + getStory().getMeta().getTitle(); + } else if (chapter == 0) { + Chapter resume = getStory().getMeta().getResume(); + if (resume != null) { + name = String.format(" %s", resume.getName()); + } else { + // TODO: i18n + name = "[No RESUME]"; + } + } else { + int max = getStory().getChapters().size(); + Chapter chap = getStory().getChapters().get(chapter - 1); + name = String.format(" %d/%d: %s", chapter, max, chap.getName()); + } + + int width = getWidth() - currentChapter.getX(); + name = String.format("%-" + width + "s", name); + if (name.length() > width) { + name = name.substring(0, width); + } + + currentChapter.setLabel(name); + } + + /** + * Set the current title in-window. + * + * @param title + * the new title + */ + private void setCurrentTitle(String title) { + String pad = ""; + if (title.length() < getWidth()) { + int padSize = (getWidth() - title.length()) / 2; + pad = String.format("%" + padSize + "s", ""); + } + + title = pad + title + pad; + titleField.setWidth(title.length()); + titleField.setLabel(title); + } + + private static String desc(MetaData meta) { + return String.format("%s: %s", meta.getLuid(), meta.getTitle()); + } + + @Override + public void onCommand(TCommandEvent command) { + if (command.getCmd().equals(TuiReaderApplication.CMD_EXIT)) { + TuiReaderApplication.close(this); + } else { + // Handle our own event if needed here + super.onCommand(command); + } + } +} -- 2.27.0