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.
Instance.init();
TuiReader.setDefaultReaderType(ReaderType.TUI);
- try {
- TuiReader.getReader().browse(null);
- } catch (IOException e) {
- Instance.getInstance().getTraceHandler().error(e);
- }
+ new TuiReader().browse(null);
}
}
--- /dev/null
+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}.
+ * <p>
+ * 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 <E>
+ * the type of {@link Bundle} to edit
+ */
+public abstract class ConfigItem<E extends Enum<E>> extends TWidget {
+ /** The code base */
+ private final ConfigItemBase<TWidget, E> 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<E> info,
+ boolean autoDirtyHandling) {
+ super(parent);
+
+ base = new ConfigItemBase<TWidget, E>(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<TWidget> reload() {
+ List<TWidget> 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<E> 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.
+ * <p>
+ * 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}.
+ * <p>
+ * 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)}.
+ * <p>
+ * 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 <E>
+ * 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 <E extends Enum<E>> ConfigItem<E> createItem(TWidget parent,
+ int x, int y, MetaInfo<E> info, int nhgap) {
+
+ ConfigItem<E> configItem;
+ switch (info.getFormat()) {
+ // TODO
+ // case BOOLEAN:
+ // configItem = new ConfigItemBoolean<E>(info);
+ // break;
+ // case COLOR:
+ // configItem = new ConfigItemColor<E>(info);
+ // break;
+ // case FILE:
+ // configItem = new ConfigItemBrowse<E>(info, false);
+ // break;
+ // case DIRECTORY:
+ // configItem = new ConfigItemBrowse<E>(info, true);
+ // break;
+ // case COMBO_LIST:
+ // configItem = new ConfigItemCombobox<E>(info, true);
+ // break;
+ // case FIXED_LIST:
+ // configItem = new ConfigItemCombobox<E>(info, false);
+ // break;
+ // case INT:
+ // configItem = new ConfigItemInteger<E>(info);
+ // break;
+ // case PASSWORD:
+ // configItem = new ConfigItemPassword<E>(info);
+ // break;
+ // case LOCALE:
+ // configItem = new ConfigItemLocale<E>(info);
+ // break;
+ // case STRING:
+ default:
+ configItem = new ConfigItemString<E>(parent, info);
+ break;
+ }
+
+ configItem.init(nhgap);
+ configItem.setX(x);
+ configItem.setY(y);
+
+ return configItem;
+ }
+}
--- /dev/null
+package be.nikiroo.fanfix_jexer.reader;
+
+import jexer.TField;
+import jexer.TWidget;
+import be.nikiroo.utils.resources.MetaInfo;
+
+class ConfigItemString<E extends Enum<E>> extends ConfigItem<E> {
+ /**
+ * Create a new {@link ConfigItemString} for the given {@link MetaInfo}.
+ *
+ * @param info
+ * the {@link MetaInfo}
+ */
+ public ConfigItemString(TWidget parent, MetaInfo<E> 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);
+ }
+}
--- /dev/null
+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<E extends Enum<E>> extends TSimpleScrollableWindow {
+ private List<MetaInfo<E>> items;
+
+ public TOptionWindow(TApplication parent, Class<E> type,
+ final Bundle<E> bundle, String title) {
+ super(parent, title, 0, 0, CENTERED | RESIZABLE);
+
+ getMainPane().addLabel(title, 0, 0);
+
+ items = new ArrayList<MetaInfo<E>>();
+ List<MetaInfo<E>> groupedItems = MetaInfo.getItems(type, bundle);
+ int y = 2;
+ for (MetaInfo<E> 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<E> item : items) {
+ item.reload();
+ }
+ }
+ });
+
+ getMainPane().addButton("Default", 15, y, new TAction() {
+ @Override
+ public void DO() {
+ Object snap = bundle.takeSnapshot();
+ bundle.reload(true);
+ for (MetaInfo<E> item : items) {
+ item.reload();
+ }
+ bundle.reload(false);
+ bundle.restoreSnapshot(snap);
+ }
+ });
+
+ getMainPane().addButton("Save", 1, y, new TAction() {
+ @Override
+ public void DO() {
+ for (MetaInfo<E> item : items) {
+ item.save(true);
+ }
+
+ try {
+ bundle.updateFile();
+ } catch (IOException e1) {
+ e1.printStackTrace();
+ }
+ }
+ });
+ }
+
+ private TWidget addItem(TWidget parent, int x, int y, MetaInfo<E> 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<E> 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);
+ }
+ }
+}
--- /dev/null
+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();
+ }
+ }
+}
--- /dev/null
+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.
+ * <p>
+ * It is expected to be on par with the GUI version.
+ *
+ * @author niki
+ */
+public class TuiReader extends BasicReader {
+ /**
+ * Will detect the backend to use.
+ * <p>
+ * Swing is the default backend on Windows and MacOS while evreything else
+ * will use XTERM unless explicitly overridden by <tt>jexer.Swing</tt> =
+ * <tt>true</tt> or <tt>false</tt>.
+ *
+ * @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.");
+ }
+ }
+}
--- /dev/null
+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.
+ * <p>
+ * 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.
+ * <p>
+ * Some shortcuts are always visible, and will be put here.
+ * <p>
+ * 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.
+ * <p>
+ * Can fail if the host is not supported.
+ *
+ * @param url
+ *
+ * @return TRUE in case of success, FALSE if the host is not supported
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ private boolean imprt(String url) throws IOException {
+ try {
+ reader.getLibrary().imprt(BasicReader.getUrl(url), null);
+ main.refreshStories();
+ return true;
+ } catch (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.
+ * <p>
+ * 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}.
+ * <p>
+ * 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();
+ }
+ }
+}
--- /dev/null
+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<MetaData> listKeys;
+ private List<String> listItems;
+ private TuiReaderApplication reader;
+
+ private Mode mode = Mode.SOURCE;
+ private String target = null;
+ private String filter = "";
+
+ private List<TSizeConstraint> sizeConstraints = new ArrayList<TSizeConstraint>();
+
+ // The 2 comboboxes used to select by source/author
+ private TComboBox selectTargetBox;
+ private TComboBox selectBox;
+
+ /**
+ * Create a new {@link TuiReaderMainWindow} without any stories in the list.
+ *
+ * @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<MetaData>();
+ listItems = new ArrayList<String>();
+
+ 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<String> selects = new ArrayList<String>();
+ selects.add("(show all)");
+ selects.add("Sources");
+ selects.add("Author");
+
+ final List<String> selectTargets = new ArrayList<String>();
+ selectTargets.add("");
+
+ TLabel lblSelect = addLabel("Select: ", 0, 0);
+
+ TAction onSelect = new TAction() {
+ @Override
+ public void DO() {
+ String smode = selectBox.getText();
+ boolean showTarget;
+ if (smode == null || smode.equals("(show all)")) {
+ showTarget = false;
+ } else if (smode.equals("Sources")) {
+ selectTargets.clear();
+ selectTargets.add("(show all)");
+ 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.
+ * <p>
+ * Will take the current settings into account (filter, source...).
+ */
+ public void refreshStories() {
+ List<MetaData> 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<MetaData>();
+ }
+
+ 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}.
+ * <p>
+ * If a filter is set, only the stories which pass the filter will be
+ * displayed.
+ *
+ * @param metas
+ * the new list of stories to display
+ */
+ private void setMetas(List<MetaData> 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
--- /dev/null
+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");
+ }
+}
--- /dev/null
+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<TButton> navigationButtons;
+ private TLabel currentChapter;
+ private List<TSizeConstraint> sizeConstraints = new ArrayList<TSizeConstraint>();
+
+ // 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<TButton>(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<String, String> 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);
+ }
+ }
+}