}
@Override
- public synchronized void setMeta(URL source, Progress pg)
+ public synchronized void setMeta(URL url, Progress pg)
throws IOException {
- BasicSupport support = BasicSupport.getSupport(source);
+ BasicSupport support = BasicSupport.getSupport(url);
if (support == null) {
- throw new IOException("URL not supported: " + source.toString());
+ throw new IOException("URL not supported: " + url.toString());
}
story = support.process(pg);
if (story == null) {
throw new IOException(
"Cannot retrieve story from external source: "
- + source.toString());
+ + url.toString());
}
meta = story.getMeta();
--- /dev/null
+package be.nikiroo.fanfix.reader.tui;
+
+import java.util.List;
+
+import jexer.TScrollableWidget;
+import jexer.TWidget;
+import jexer.event.TResizeEvent;
+import jexer.event.TResizeEvent.Type;
+
+public class TSizeConstraint {
+ private TWidget widget;
+ private Integer x1;
+ private Integer y1;
+ private Integer x2;
+ private Integer y2;
+
+ // TODO: include in the window classes I use?
+
+ public TSizeConstraint(TWidget widget, Integer x1, Integer y1, Integer x2,
+ Integer y2) {
+ this.widget = widget;
+ this.x1 = x1;
+ this.y1 = y1;
+ this.x2 = x2;
+ this.y2 = y2;
+ }
+
+ public TWidget getWidget() {
+ return widget;
+ }
+
+ public Integer getX1() {
+ if (x1 != null && x1 < 0)
+ return widget.getParent().getWidth() + x1;
+ return x1;
+ }
+
+ public Integer getY1() {
+ if (y1 != null && y1 < 0)
+ return widget.getParent().getHeight() + y1;
+ return y1;
+ }
+
+ public Integer getX2() {
+ if (x2 != null && x2 <= 0)
+ return widget.getParent().getWidth() - 2 + x2;
+ return x2;
+ }
+
+ public Integer getY2() {
+ if (y2 != null && y2 <= 0)
+ return widget.getParent().getHeight() - 2 + y2;
+ return y2;
+ }
+
+ // coordinates < 0 = from the other side, x2 or y2 = 0 = max size
+ static void setSize(List<TSizeConstraint> sizeConstraints, TWidget child,
+ Integer x1, Integer y1, Integer x2, Integer y2) {
+ sizeConstraints.add(new TSizeConstraint(child, x1, y1, x2, y2));
+ }
+
+ static void resize(List<TSizeConstraint> sizeConstraints) {
+ for (TSizeConstraint sizeConstraint : sizeConstraints) {
+ TWidget widget = sizeConstraint.getWidget();
+ Integer x1 = sizeConstraint.getX1();
+ Integer y1 = sizeConstraint.getY1();
+ Integer x2 = sizeConstraint.getX2();
+ Integer y2 = sizeConstraint.getY2();
+
+ if (x1 != null)
+ widget.setX(x1);
+ if (y1 != null)
+ widget.setY(y1);
+
+ if (x2 != null)
+ widget.setWidth(x2 - widget.getX());
+ if (y2 != null)
+ widget.setHeight(y2 - widget.getY());
+
+ // Resize the text field
+ // TODO: why setW/setH/reflow not enough for the scrollbars?
+ widget.onResize(new TResizeEvent(Type.WIDGET, widget.getWidth(),
+ widget.getHeight()));
+
+ if (widget instanceof TScrollableWidget) {
+ ((TScrollableWidget) widget).reflowData();
+ }
+ }
+ }
+}
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.TStatusBar;
import jexer.TWidget;
import jexer.TWindow;
+import jexer.event.TCommandEvent;
import jexer.event.TMenuEvent;
import jexer.menu.TMenu;
import be.nikiroo.fanfix.Instance;
private Reader reader;
private TuiReaderMainWindow main;
-
- private MetaData meta;
private String source;
- private boolean useMeta;
// start reading if meta present
public TuiReaderApplication(Reader reader, BackendType backend)
super(backend);
init(reader);
- showMain(getMeta(), null, true);
+ MetaData meta = getMeta();
+ if (meta != null) {
+ read();
+ }
}
public TuiReaderApplication(Reader reader, String source,
super(backend);
init(reader);
- showMain(null, source, false);
+ showMain();
+ setSource(source);
}
@Override
public void read() throws IOException {
- MetaData meta = getMeta();
-
- if (meta == null) {
- throw new IOException("No story to read");
- }
-
- // TODO: open in editor + external option
- if (!meta.isImageDocument()) {
- TWindow window = new TuiReaderStoryWindow(this, getLibrary(), meta,
- getChapter());
- window.maximize();
- } else {
- try {
- openExternal(getLibrary(), meta.getLuid());
- } catch (IOException e) {
- messageBox("Error when trying to open the story",
- e.getMessage(), TMessageBox.Type.OK);
- }
- }
+ read(getStory(null));
}
@Override
reader.setChapter(chapter);
}
+ public void read(Story story) throws IOException {
+ if (story == null) {
+ throw new IOException("No story to read");
+ }
+
+ // TODO: open in editor + external option
+ if (!story.getMeta().isImageDocument()) {
+ TWindow window = new TuiReaderStoryWindow(this, story, getChapter());
+ window.maximize();
+ } else {
+ try {
+ openExternal(getLibrary(), story.getMeta().getLuid());
+ } catch (IOException e) {
+ messageBox("Error when trying to open the story",
+ e.getMessage(), TMessageBox.Type.OK);
+ }
+ }
+ }
+
/**
* Set the default status bar when this window appear.
* <p>
}
- private void showMain(MetaData meta, String source, boolean useMeta)
- throws IOException {
- // TODO: thread-safety
- this.meta = meta;
- this.source = source;
- this.useMeta = useMeta;
-
+ private void showMain() {
if (main != null && main.isVisible()) {
main.activate();
} else {
main.close();
}
main = new TuiReaderMainWindow(this);
- if (useMeta) {
- main.setMeta(meta);
- if (meta != null) {
- read();
- }
- } else {
- main.setSource(source);
- }
+ main.maximize();
}
}
+ private void setSource(String source) {
+ this.source = source;
+ showMain();
+ main.setSource(source);
+ }
+
private void init(Reader reader) {
this.reader = reader;
getBackend().setTitle("Fanfix");
}
+ @Override
+ protected boolean onCommand(TCommandEvent command) {
+ if (command.getCmd().equals(TuiReaderMainWindow.CMD_SEARCH)) {
+ messageBox("title", "caption");
+ return true;
+ }
+ return super.onCommand(command);
+ }
+
@Override
protected boolean onMenu(TMenuEvent menu) {
// TODO: i18n
switch (menu.getId()) {
case MENU_EXIT:
close(this);
+ return true;
+ case MENU_OPEN:
+ String openfile = null;
+ try {
+ openfile = fileOpenBox(".");
+ reader.setMeta(BasicReader.getUrl(openfile), null);
+ read();
+ } catch (IOException e) {
+ // TODO: i18n
+ error("Fail to open file"
+ + (openfile == null ? "" : ": " + openfile),
+ "Import error", e);
+ }
+
return true;
case MENU_IMPORT_URL:
String clipboard = "";
String url = inputBox("Import story", "URL to import", clipboard)
.getText();
- if (!imprt(url)) {
- // TODO: bad import
+ try {
+ if (!imprt(url)) {
+ // TODO: i18n
+ error("URK not supported: " + url, "Import error");
+ }
+ } catch (IOException e) {
+ // TODO: i18n
+ error("Fail to import URL: " + url, "Import error", e);
}
return true;
case MENU_IMPORT_FILE:
+ String filename = null;
try {
- String filename = fileOpenBox(".");
+ filename = fileOpenBox(".");
if (!imprt(filename)) {
- // TODO: bad import
+ // TODO: i18n
+ error("File not supported: " + filename, "Import error");
}
} catch (IOException e) {
- // TODO: bad file
- e.printStackTrace();
+ // TODO: i18n
+ error("Fail to import file"
+ + (filename == null ? "" : ": " + filename),
+ "Import error", e);
}
-
return true;
case MENU_LIBRARY:
- try {
- showMain(meta, source, useMeta);
- } catch (IOException e) {
- e.printStackTrace();
- }
-
+ showMain();
+ setSource(source);
return true;
}
return super.onMenu(menu);
}
- private boolean imprt(String url) {
+ /**
+ * Import the given url.
+ * <p>
+ * Can fail if the host is not supported.
+ *
+ * @param url
+ *
+ * @return TRUE in case of success, FALSE if the host is not supported
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ private boolean imprt(String url) throws IOException {
try {
reader.getLibrary().imprt(BasicReader.getUrl(url), null);
main.refreshStories();
return true;
- } catch (IOException e) {
+ } catch (UnknownHostException e) {
return false;
}
}
reader.openExternal(lib, luid);
}
+ /**
+ * Display an error message and log it.
+ *
+ * @param message
+ * the message
+ * @param title
+ * the title of the error message
+ */
+ private void error(String message, String title) {
+ error(message, title, null);
+ }
+
+ /**
+ * Display an error message and log it, including the linked
+ * {@link Exception}.
+ *
+ * @param message
+ * the message
+ * @param title
+ * the title of the error message
+ * @param e
+ * the exception to log if any (can be NULL)
+ */
+ private void error(String message, String title, Exception e) {
+ Instance.getTraceHandler().error(title + ": " + message);
+ if (e != null) {
+ Instance.getTraceHandler().error(e);
+ }
+
+ if (e != null) {
+ messageBox(title, message //
+ + "\n" + e.getMessage());
+ } else {
+ messageBox(title, message);
+ }
+ }
+
/**
* Ask the user and, if confirmed, close the {@link TApplication} this
* {@link TWidget} is running on.
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;
* @author niki
*/
class TuiReaderMainWindow extends TWindow {
+ public static final int MENU_SEARCH = 1100;
+ public static final TCommand CMD_SEARCH = new TCommand(MENU_SEARCH) {
+ };
+
private TList list;
private List<MetaData> listKeys;
private List<String> listItems;
private Reader reader;
private String source;
+ private String filter = "";
+ private List<TSizeConstraint> sizeConstraints = new ArrayList<TSizeConstraint>();
+
+ // TODO: because no way to find out the current index!!
+ private TComboBox select;
/**
* Create a new {@link TuiReaderMainWindow} without any stories in the list.
this.reader = reader;
- maximize();
-
listKeys = new ArrayList<MetaData>();
listItems = new ArrayList<String>();
- list = addList(listItems, 0, 0, getWidth(), getHeight(), new TAction() {
- @Override
- public void DO() {
- MetaData meta = getSelectedMeta();
- if (meta != null) {
- readStory(meta);
- }
- }
- });
- // TODO: add the current "source/type" or filter
- reader.setStatusBar(this, "Library");
+ addSearch();
+ addList();
+ addSelect(); // TODO: last so it can draw over the rest
+
+ TStatusBar statusBar = reader.setStatusBar(this, "Library");
+ statusBar.addShortcutKeypress(TKeypress.kbCtrlF, CMD_SEARCH, "Search");
+
+ TSizeConstraint.resize(sizeConstraints);
// TODO: remove when not used anymore
// 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: make a full list
+ final List<String> options = new ArrayList<String>();
+ options.add("(all opt)");
+ options.add("Sources");
+ options.add("Author");
+
+ // TODO
+ final List<String> sources = new ArrayList<String>();
+ sources.add("(show all)");
+ for (String source : reader.getLibrary().getSources()) {
+ sources.add(source);
+ }
+
+ TLabel lblSelect = addLabel("Select: ", 0, 0);
+
+ // TODO: why must be last so to be able to draw over the rest
+ // TODO: make it so we cannot add manual entries
+ // TODO: how to select the item via keyboard? why double-click via
+ // mouse?
+ // TODO: how to change the values size on resize?
+ // TODO: setWidth() does not impact the display width, only the control
+ // and the down arrow on the right
+ // TODO: width 1 +resize + click on down arrow = bad format exception
+ select = addComboBox(0, 0, 10, sources, 0,
+ Math.min(sources.size() + 1, getHeight() - 1 - 1),
+ new TAction() {
+ @Override
+ public void DO() {
+ // TODO: wut?
+ if (select.getText().equals("(show all)")) {
+ setSource(null);
+ } else {
+ setSource(select.getText());
+ }
+ refreshStories();
+ }
+ });
+
+ final TComboBox option = addComboBox(0, 0, 10, options, 0,
+ Math.min(sources.size() + 1, getHeight() - 1 - 1),
+ new TAction() {
+ @Override
+ public void DO() {
+ // TODO Not working:
+ sources.clear();
+ sources.add("(show all)");
+ for (String source : reader.getLibrary().getSources()) {
+ sources.add(source);
+ }
+ }
+ });
+
+ TSizeConstraint.setSize(sizeConstraints, lblSelect, 5, 3, null, null);
+ TSizeConstraint.setSize(sizeConstraints, option, 15, 3, -5, null);
+ TSizeConstraint.setSize(sizeConstraints, select, 15, 4, -5, null);
+ }
+
+ @Override
+ public void onResize(TResizeEvent resize) {
+ super.onResize(resize);
+ TSizeConstraint.resize(sizeConstraints);
+ }
+
@Override
public void onClose() {
setVisible(false);
}
/**
- * Change the source filter and display all stories matching this source.
- *
- * @param source
- * the new source or NULL for all sources
+ * Refresh the list of stories displayed in this library.
+ * <p>
+ * Will take the current settings into account (filter, source...).
*/
- public void setSource(String source) {
- this.source = source;
- refreshStories();
- }
-
public void refreshStories() {
List<MetaData> metas = reader.getLibrary().getListBySource(source);
setMetas(metas);
}
/**
- * Update the list of stories displayed in this {@link TWindow}.
+ * Change the source filter and display all stories matching this source.
*
- * @param meta
- * the new (unique) story to display
+ * @param source
+ * the new source or NULL for all sources
*/
- public void setMeta(MetaData meta) {
- List<MetaData> metas = new ArrayList<MetaData>();
- if (meta != null) {
- metas.add(meta);
- }
-
- setMetas(metas);
+ public void setSource(String source) {
+ this.source = source;
+ 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
if (metas != null) {
for (MetaData meta : metas) {
- listKeys.add(meta);
- listItems.add(desc(meta));
+ String desc = desc(meta);
+ if (filter.isEmpty()
+ || desc.toLowerCase().contains(filter.toLowerCase())) {
+ listKeys.add(meta);
+ listItems.add(desc);
+ }
}
}
}
return;
+
case -1:
try {
reader.getLibrary().delete(meta.getLuid());
import jexer.TWindow;
import jexer.event.TCommandEvent;
import jexer.event.TResizeEvent;
-import jexer.event.TResizeEvent.Type;
import be.nikiroo.fanfix.data.Chapter;
import be.nikiroo.fanfix.data.MetaData;
import be.nikiroo.fanfix.data.Paragraph;
import be.nikiroo.fanfix.data.Paragraph.ParagraphType;
import be.nikiroo.fanfix.data.Story;
-import be.nikiroo.fanfix.library.BasicLibrary;
import be.nikiroo.utils.StringUtils;
/**
* @author niki
*/
class TuiReaderStoryWindow extends TWindow {
- private BasicLibrary lib;
- private MetaData meta;
private Story story;
private TLabel titleField;
private TText textField;
private int chapter = -99; // invalid value
private List<TButton> navigationButtons;
private TLabel currentChapter;
+ private List<TSizeConstraint> sizeConstraints = new ArrayList<TSizeConstraint>();
// chapter: -1 for "none" (0 is desc)
- public TuiReaderStoryWindow(TuiReaderApplication app, BasicLibrary lib,
- MetaData meta, int chapter) {
- super(app, desc(meta), 0, 0, 60, 18, CENTERED | RESIZABLE);
+ public TuiReaderStoryWindow(TuiReaderApplication app, Story story,
+ int chapter) {
+ super(app, desc(story.getMeta()), 0, 0, 60, 18, CENTERED | RESIZABLE);
- this.lib = lib;
- this.meta = meta;
+ this.story = story;
- app.setStatusBar(this, desc(meta));
+ app.setStatusBar(this, desc(story.getMeta()));
// last = use window background
titleField = new TLabel(this, " Title", 0, 1, "tlabel", false);
- textField = new TText(this, "", 1, 3, getWidth() - 4, getHeight() - 5);
- table = new TTable(this, 0, 3, getWidth(), getHeight() - 4, null, null,
- Arrays.asList("Key", "Value"), true);
+ textField = new TText(this, "", 0, 0, 1, 1);
+ table = new TTable(this, 0, 0, 1, 1, null, null, Arrays.asList("Key",
+ "Value"), true);
titleField.setEnabled(false);
- textField.getVerticalScroller().setX(
- textField.getVerticalScroller().getX() + 1);
navigationButtons = new ArrayList<TButton>(5);
- // -3 because 0-based and 2 for borders
- int row = getHeight() - 3;
-
// for bg colour when << button is pressed
- navigationButtons.add(addButton(" ", 0, row, null));
- navigationButtons.add(addButton("<< ", 0, row, new TAction() {
+ navigationButtons.add(addButton(" ", 0, 0, null));
+ navigationButtons.add(addButton("<< ", 0, 0, new TAction() {
@Override
public void DO() {
setChapter(-1);
}
}));
- navigationButtons.add(addButton("< ", 4, row, new TAction() {
+ navigationButtons.add(addButton("< ", 4, 0, new TAction() {
@Override
public void DO() {
setChapter(TuiReaderStoryWindow.this.chapter - 1);
}
}));
- navigationButtons.add(addButton("> ", 7, row, new TAction() {
+ navigationButtons.add(addButton("> ", 7, 0, new TAction() {
@Override
public void DO() {
setChapter(TuiReaderStoryWindow.this.chapter + 1);
}
}));
- navigationButtons.add(addButton(">> ", 10, row, new TAction() {
+ navigationButtons.add(addButton(">> ", 10, 0, new TAction() {
@Override
public void DO() {
setChapter(getStory().getChapters().size());
navigationButtons.get(1).setEnabled(false);
navigationButtons.get(2).setEnabled(false);
- currentChapter = addLabel("", 14, row);
- currentChapter.setWidth(getWidth() - 10);
+ currentChapter = addLabel("", 0, 0);
+
+ TSizeConstraint.setSize(sizeConstraints, textField, 1, 3, -1, 0);
+ TSizeConstraint.setSize(sizeConstraints, table, 0, 3, 0, 0);
+ TSizeConstraint.setSize(sizeConstraints, currentChapter, 14, -3, -1,
+ null);
+
+ for (TButton navigationButton : navigationButtons) {
+ TSizeConstraint.setSize(sizeConstraints, navigationButton, null,
+ -3, null, null);
+ }
+
+ onResize(null);
setChapter(chapter);
}
@Override
public void onResize(TResizeEvent resize) {
- super.onResize(resize);
-
- // Resize the text field
- // TODO: why setW/setH/reflow not enough for the scrollbars?
- textField.onResize(new TResizeEvent(Type.WIDGET, resize.getWidth() - 4,
- resize.getHeight() - 5));
- textField.getVerticalScroller().setX(
- textField.getVerticalScroller().getX() + 1);
+ if (resize != null) {
+ super.onResize(resize);
+ }
- table.setWidth(getWidth());
- table.setHeight(getHeight() - 4);
- table.reflowData();
+ // TODO: find out why TText and TTable does not behave the same way
+ // (offset of 2 for height and width)
- // -3 because 0-based and 2 for borders
- int row = getHeight() - 3;
+ TSizeConstraint.resize(sizeConstraints);
- String name = currentChapter.getLabel();
- while (name.length() < resize.getWidth() - currentChapter.getX()) {
- name += " ";
- }
- currentChapter.setLabel(name);
- currentChapter.setWidth(resize.getWidth() - 10);
- currentChapter.setY(row);
+ textField.getVerticalScroller().setX(
+ textField.getVerticalScroller().getX() + 1);
- for (TButton button : navigationButtons) {
- button.setY(row);
- }
+ setCurrentChapterText();
}
/**
}
private Story getStory() {
- if (story == null) {
- // TODO: progress bar?
- story = lib.getStory(meta.getLuid(), null);
- }
return story;
}
}
int width = getWidth() - currentChapter.getX();
- while (name.length() < width) {
- name += " ";
- }
-
+ name = String.format("%-" + width + "s", name);
if (name.length() > width) {
name = name.substring(0, width);
}
};
}
- List<String> resultFiles = Arrays.asList(resultDir.list(filter));
- resultFiles.sort(null);
- List<String> expectedFiles = Arrays.asList(expectedDir.list(filter));
- expectedFiles.sort(null);
+ List<String> resultFiles;
+ List<String> expectedFiles;
+ {
+ String[] resultArr = resultDir.list(filter);
+ Arrays.sort(resultArr);
+ resultFiles = Arrays.asList(resultArr);
+ String[] expectedArr = expectedDir.list(filter);
+ Arrays.sort(expectedArr);
+ expectedFiles = Arrays.asList(expectedArr);
+ }
testCase.assertEquals(errMess, expectedFiles, resultFiles);