Add a new TUI system based upon Jexer (WIP)
authorNiki Roo <niki@nikiroo.be>
Tue, 27 Jun 2017 16:25:20 +0000 (18:25 +0200)
committerNiki Roo <niki@nikiroo.be>
Tue, 27 Jun 2017 16:25:20 +0000 (18:25 +0200)
15 files changed:
Makefile.base
README.md
changelog.md
libs/jexer-0.0.4-sources.jar [new file with mode: 0644]
libs/jexer-0.0.4-sources.patch.jar [new file with mode: 0644]
src/be/nikiroo/fanfix/Main.java
src/be/nikiroo/fanfix/bundles/Config.java
src/be/nikiroo/fanfix/bundles/config.properties
src/be/nikiroo/fanfix/bundles/resources.properties
src/be/nikiroo/fanfix/reader/BasicReader.java
src/be/nikiroo/fanfix/reader/LocalReader.java
src/be/nikiroo/fanfix/reader/TuiReader.java [new file with mode: 0644]
src/be/nikiroo/fanfix/reader/TuiReaderApplication.java [new file with mode: 0644]
src/be/nikiroo/fanfix/reader/TuiReaderMainWindow.java [new file with mode: 0644]
src/be/nikiroo/fanfix/reader/TuiReaderStoryWindow.java [new file with mode: 0644]

index 54dbf1aa548f6a1b9e8e1ee81e2ea1fde4bc03aa..ef0b92616fcb8a1c2b0c118bea06c326b7219e4a 100644 (file)
@@ -108,7 +108,7 @@ test-resources: resources
 
 libs: bin
        @[ -e bin/libs -o ! -d libs ] || echo Extracting sources from libs...
-       @[ -e bin/libs -o ! -d libs ] || (cd src && for lib in ../libs/*-sources.jar; do \
+       @[ -e bin/libs -o ! -d libs ] || (cd src && for lib in ../libs/*-sources.jar ../libs/*-sources.patch.jar; do \
                basename "$$lib"; \
                jar xf "$$lib"; \
        done )
index f66be31a85fda8501b02eda5a08f94d766d15e8c..826aacb2acf5b971db72964d9a823cbe15300211 100644 (file)
--- a/README.md
+++ b/README.md
@@ -111,6 +111,12 @@ Currently missing, but either in progress or planned:
     - [x] ..as a screen view
   - [x] options screen
 - [ ] A TUI reader
+  - [x] Choose an output (Jexer)
+  - [x] Implement it from --set-reader to the actual window
+  - [x] List the stories
+  - [ ] Fix the UI layout
+  - [ ] Status bar and real menus
+  - [ ] Open a story in the reader and/or natively
 - [ ] Check if it can work on Android
   - [x] First checks: it should work, but with changes
   - [ ] Adapt work on images :(
index f399defa426c51c6bf878da54f230a43feb005ec..89b6b00d44b23dca262852414a71a8be831f2559 100644 (file)
@@ -1,5 +1,8 @@
 # Fanfix
 
+## Version WIP
+- New reader type: TUI (a text user interface with windows and menus)
+
 ## Version 1.5.3
 - FimFiction: Fix tags and chapter handling for some stories
 
diff --git a/libs/jexer-0.0.4-sources.jar b/libs/jexer-0.0.4-sources.jar
new file mode 100644 (file)
index 0000000..b0819e0
Binary files /dev/null and b/libs/jexer-0.0.4-sources.jar differ
diff --git a/libs/jexer-0.0.4-sources.patch.jar b/libs/jexer-0.0.4-sources.patch.jar
new file mode 100644 (file)
index 0000000..85dc502
Binary files /dev/null and b/libs/jexer-0.0.4-sources.patch.jar differ
index a282e7ccef83f7125735bf7bb810cc8e9dad4993..5cd3313897317d7fb5a03c73c17557ff421ae890 100644 (file)
@@ -52,8 +52,8 @@ public class Main {
         * <li>--read-url [URL] ([chapter number]): convert on the fly and read the
         * story, without saving it</li>
         * <li>--list ([type]): list the stories present in the library</li>
-        * <li>--set-reader [reader type]: set the reader type to CLI or LOCAL for
-        * this command</li>
+        * <li>--set-reader [reader type]: set the reader type to CLI, TUI or LOCAL
+        * for this command</li>
         * <li>--version: get the version of the program</li>
         * </ul>
         * 
@@ -157,6 +157,7 @@ public class Main {
                                break;
                        case SET_READER:
                                exitCode = setReaderType(args[i]);
+                               action = MainAction.START;
                                break;
                        case START:
                                exitCode = 255; // not supposed to be selected by user
@@ -246,8 +247,7 @@ public class Main {
                                updates.ok(); // we consider it read
                                break;
                        case START:
-                               UIUtils.setLookAndFeel();
-                               BasicReader.setDefaultReaderType(ReaderType.LOCAL);
+                               //BasicReader.setDefaultReaderType(ReaderType.LOCAL);
                                BasicReader.getReader().start(null);
                                break;
                        }
index a38b9e2d114327fb6f77c58efec92d7c200b05c7..63e6465a3bdb6f7da95427098982ba6a100f0b29 100644 (file)
@@ -12,8 +12,8 @@ import be.nikiroo.utils.resources.Meta.Format;
 public enum Config {
        @Meta(description = "language (example: en-GB, fr-BE...) or nothing for default system language", format = Format.LOCALE, info = "Force the language (can be overwritten again with the env variable $LANG)")
        LANG, //
-       @Meta(description = "reader type (CLI = simple output to console, LOCAL = use local system file handler)", format = Format.FIXED_LIST, list = {
-                       "CLI", "LOCAL" }, info = "Select the default reader to use to read stories")
+       @Meta(description = "reader type (CLI = simple output to console, TUI = Text User Interface with menus and windows, GUI = a GUI with locally stored files)", format = Format.FIXED_LIST, list = {
+                       "CLI", "GUI", "TUI" }, info = "Select the default reader to use to read stories")
        READER_TYPE, //
        @Meta(description = "absolute path, $HOME variable supported, / is always accepted as dir separator", format = Format.DIRECTORY, info = "The directory where to store temporary files, defaults to directory 'tmp' in the conig directory (usually $HOME/.fanfix)")
        CACHE_DIR, //
index 6151843c7660ae82c12f72fe5922de6454cffd33..061b0fbf6b3459c60e3da9d65cb328e8292e2fd2 100644 (file)
@@ -5,9 +5,9 @@
 # language (example: en-GB, fr-BE...) or nothing for default system language
 # (FORMAT: LOCALE) Force the language (can be overwritten again with the env variable $LANG)
 LANG = 
-# reader type (CLI = simple output to console, LOCAL = use local system file handler)
+# reader type (CLI = simple output to console, TUI = Text User Interface with menus and windows, GUI = a GUI with locally stored files)
 # (FORMAT: FIXED_LIST) Select the default reader to use to read stories
-# ALLOWED VALUES: "CLI" "LOCAL"
+# ALLOWED VALUES: "CLI" "GUI" "TUI"
 READER_TYPE = 
 # absolute path, $HOME variable supported, / is always accepted as dir separator
 # (FORMAT: DIRECTORY) The directory where to store temporary files, defaults to directory 'tmp' in the conig directory (usually $HOME/.fanfix)
index f664eb45ef5cf45c3ca7536abcc6c830efb347e5..a63fbd3e47f5782bd6bb562623bbfad944d320d1 100644 (file)
@@ -16,7 +16,7 @@ HELP_SYNTAX = Valid options:\n\
 \t--read [id] ([chapter number]): read the given story from the library\n\
 \t--read-url [URL] ([chapter number]): convert on the fly and read the story, without saving it\n\
 \t--list: list the stories present in the library\n\
-\t--set-reader [reader type]: set the reader type to CLI or LOCAL for this command\n\
+\t--set-reader [reader type]: set the reader type to CLI, TUI or GUI for this command\n\
 \t--help: this help message\n\
 \t--version: return the version of the program\n\
 \n\
index ae1c8d4c6eb32162c65421e0959da5bb6fc00e31..74bd5d48f98000606f9d8c72c67a88ea6f80403a 100644 (file)
@@ -1,5 +1,6 @@
 package be.nikiroo.fanfix.reader;
 
+import java.awt.Desktop;
 import java.io.File;
 import java.io.IOException;
 import java.net.MalformedURLException;
@@ -8,9 +9,12 @@ import java.net.URL;
 import be.nikiroo.fanfix.Instance;
 import be.nikiroo.fanfix.Library;
 import be.nikiroo.fanfix.bundles.Config;
+import be.nikiroo.fanfix.bundles.UiConfig;
+import be.nikiroo.fanfix.data.MetaData;
 import be.nikiroo.fanfix.data.Story;
 import be.nikiroo.fanfix.supported.BasicSupport;
 import be.nikiroo.utils.Progress;
+import be.nikiroo.utils.ui.UIUtils;
 
 /**
  * The class that handles the different {@link Story} readers you can use.
@@ -24,10 +28,12 @@ public abstract class BasicReader {
                /** Simple reader that outputs everything on the console */
                CLI,
                /** Reader that starts local programs to handle the stories */
-               LOCAL
+               GUI,
+               /** A text (UTF-8) reader with menu and text windows */
+               TUI,
        }
 
-       private static ReaderType defaultType = ReaderType.CLI;
+       private static ReaderType defaultType = ReaderType.GUI;
        private Story story;
        private ReaderType type;
 
@@ -162,10 +168,13 @@ public abstract class BasicReader {
                try {
                        if (defaultType != null) {
                                switch (defaultType) {
-                               case LOCAL:
-                                       return new LocalReader().setType(ReaderType.LOCAL);
+                               case GUI:
+                                       UIUtils.setLookAndFeel();
+                                       return new LocalReader().setType(ReaderType.GUI);
                                case CLI:
                                        return new CliReader().setType(ReaderType.CLI);
+                               case TUI:
+                                       return new TuiReader().setType(ReaderType.TUI);
                                }
                        }
                } catch (IOException e) {
@@ -224,4 +233,41 @@ public abstract class BasicReader {
 
                return source;
        }
+
+       // open with external player the related file
+       public static void open(String luid) throws IOException {
+               MetaData meta = Instance.getLibrary().getInfo(luid);
+               File target = Instance.getLibrary().getFile(luid);
+
+               open(meta, target);
+       }
+
+       // open with external player the related file
+       protected static void open(MetaData meta, File target) throws IOException {
+               String program = null;
+               if (meta.isImageDocument()) {
+                       program = Instance.getUiConfig().getString(
+                                       UiConfig.IMAGES_DOCUMENT_READER);
+               } else {
+                       program = Instance.getUiConfig().getString(
+                                       UiConfig.NON_IMAGES_DOCUMENT_READER);
+               }
+
+               if (program != null && program.trim().isEmpty()) {
+                       program = null;
+               }
+
+               if (program == null) {
+                       try {
+                               Desktop.getDesktop().browse(target.toURI());
+                       } catch (UnsupportedOperationException e) {
+                               Runtime.getRuntime().exec(
+                                               new String[] { "xdg-open", target.getAbsolutePath() });
+
+                       }
+               } else {
+                       Runtime.getRuntime().exec(
+                                       new String[] { program, target.getAbsolutePath() });
+               }
+       }
 }
index a3fdcab7b67d90d9404f4b77f101a0fb2d4531df..2b92e721db959fc52aa46af7ebb7799daa4afec5 100644 (file)
@@ -16,7 +16,6 @@ import be.nikiroo.fanfix.Instance;
 import be.nikiroo.fanfix.Library;
 import be.nikiroo.fanfix.VersionCheck;
 import be.nikiroo.fanfix.bundles.UiConfig;
-import be.nikiroo.fanfix.data.MetaData;
 import be.nikiroo.fanfix.data.Story;
 import be.nikiroo.fanfix.output.BasicOutput.OutputType;
 import be.nikiroo.utils.Progress;
@@ -111,29 +110,6 @@ class LocalReader extends BasicReader {
                }
        }
 
-       /**
-        * Get the target file related to this {@link Story}.
-        * 
-        * @param luid
-        *            the LUID of the {@link Story}
-        * @param pg
-        *            the optional progress reporter
-        * 
-        * @return the target file
-        * 
-        * @throws IOException
-        *             in case of I/O error
-        */
-       public File getTarget(String luid, Progress pg) throws IOException {
-               File file = lib.getFile(luid);
-               if (file == null) {
-                       imprt(luid, pg);
-                       file = lib.getFile(luid);
-               }
-
-               return file;
-       }
-
        /**
         * Check if the {@link Story} denoted by this Library UID is present in the
         * {@link LocalReader} cache.
@@ -224,35 +200,13 @@ class LocalReader extends BasicReader {
 
        // open the given book
        void open(String luid, Progress pg) throws IOException {
-               MetaData meta = Instance.getLibrary().getInfo(luid);
-               File target = getTarget(luid, pg);
-
-               String program = null;
-               if (meta.isImageDocument()) {
-                       program = Instance.getUiConfig().getString(
-                                       UiConfig.IMAGES_DOCUMENT_READER);
-               } else {
-                       program = Instance.getUiConfig().getString(
-                                       UiConfig.NON_IMAGES_DOCUMENT_READER);
-               }
-
-               if (program != null && program.trim().isEmpty()) {
-                       program = null;
+               File file = lib.getFile(luid);
+               if (file == null) {
+                       imprt(luid, pg);
+                       file = lib.getFile(luid);
                }
 
-               if (program == null) {
-                       try {
-                               Desktop.getDesktop().browse(target.toURI());
-                       } catch (UnsupportedOperationException e) {
-                               Runtime.getRuntime().exec(
-                                               new String[] { "xdg-open", target.getAbsolutePath() });
-
-                       }
-               } else {
-                       Runtime.getRuntime().exec(
-                                       new String[] { program, target.getAbsolutePath() });
-
-               }
+               open(Instance.getLibrary().getInfo(luid), file);
        }
 
        void changeType(String luid, String newType) {
diff --git a/src/be/nikiroo/fanfix/reader/TuiReader.java b/src/be/nikiroo/fanfix/reader/TuiReader.java
new file mode 100644 (file)
index 0000000..d18436f
--- /dev/null
@@ -0,0 +1,35 @@
+package be.nikiroo.fanfix.reader;
+
+import java.io.IOException;
+import java.util.List;
+
+import be.nikiroo.fanfix.Instance;
+import be.nikiroo.fanfix.data.MetaData;
+
+class TuiReader extends BasicReader {
+       @Override
+       public void read() throws IOException {
+               if (getStory() == null) {
+                       throw new IOException("No story to read");
+               }
+
+               open(getStory().getMeta().getLuid());
+       }
+
+       @Override
+       public void read(int chapter) throws IOException {
+               // TODO: show a special page?
+               read();
+       }
+
+       @Override
+       public void start(String type) {
+               List<MetaData> stories = Instance.getLibrary().getListByType(type);
+               try {
+                       TuiReaderApplication app = new TuiReaderApplication(stories, this);
+                       new Thread(app).start();
+               } catch (Exception e) {
+                       Instance.syserr(e);
+               }
+       }
+}
diff --git a/src/be/nikiroo/fanfix/reader/TuiReaderApplication.java b/src/be/nikiroo/fanfix/reader/TuiReaderApplication.java
new file mode 100644 (file)
index 0000000..3e9d1d7
--- /dev/null
@@ -0,0 +1,75 @@
+package be.nikiroo.fanfix.reader;
+
+import java.io.IOException;
+import java.util.List;
+
+import jexer.TApplication;
+import jexer.TMessageBox;
+import be.nikiroo.fanfix.Instance;
+import be.nikiroo.fanfix.data.MetaData;
+
+public class TuiReaderApplication extends TApplication {
+       private BasicReader reader;
+
+       private static BackendType guessBackendType() {
+               // Swing is the default backend on Windows unless explicitly
+               // overridden by jexer.Swing.
+               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;
+       }
+
+       public TuiReaderApplication(List<MetaData> stories, BasicReader reader)
+                       throws Exception {
+               this(stories, reader, guessBackendType());
+       }
+
+       public TuiReaderApplication(List<MetaData> stories, BasicReader reader,
+                       TApplication.BackendType backend) throws Exception {
+               super(backend);
+
+               this.reader = reader;
+
+               // Add the menus
+               addFileMenu();
+               addEditMenu();
+               addWindowMenu();
+               addHelpMenu();
+
+               getBackend().setTitle("Testy");
+
+               new TuiReaderMainWindow(this, stories);
+       }
+
+       public void open(MetaData meta) {
+               // TODO: open in editor + external option
+               if (true) {
+                       if (!meta.isImageDocument()) {
+                               new TuiReaderStoryWindow(this, meta);
+                       } else {
+                               messageBox("Error when trying to open the story",
+                                               "Images document not yet supported.",
+                                               TMessageBox.Type.OK);
+                       }
+                       return;
+               }
+               try {
+                       reader.open(meta.getLuid());
+               } catch (IOException e) {
+                       messageBox("Error when trying to open the story", e.getMessage(),
+                                       TMessageBox.Type.OK);
+               }
+       }
+}
diff --git a/src/be/nikiroo/fanfix/reader/TuiReaderMainWindow.java b/src/be/nikiroo/fanfix/reader/TuiReaderMainWindow.java
new file mode 100644 (file)
index 0000000..1de9488
--- /dev/null
@@ -0,0 +1,91 @@
+package be.nikiroo.fanfix.reader;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import jexer.TAction;
+import jexer.TList;
+import jexer.TRadioGroup;
+import jexer.TTreeItem;
+import jexer.TTreeView;
+import jexer.TWindow;
+import be.nikiroo.fanfix.data.MetaData;
+
+public class TuiReaderMainWindow extends TWindow {
+       private TList list;
+       private List<MetaData> listKeys;
+       private List<String> listItems;
+       private TuiReaderApplication reader;
+
+       /**
+        * Constructor.
+        * 
+        * @param parent
+        *            the main application
+        * @param flags
+        *            bitmask of MODAL, CENTERED, or RESIZABLE
+        */
+       public TuiReaderMainWindow(TuiReaderApplication reader,
+                       List<MetaData> stories) {
+               // Construct a demo window. X and Y don't matter because it will be
+               // centered on screen.
+               super(reader, "Demo Window", 0, 0, 60, 18, CENTERED | RESIZABLE
+                               | UNCLOSABLE);
+
+               this.reader = reader;
+
+               maximize();
+
+               listKeys = new ArrayList<MetaData>();
+               listItems = new ArrayList<String>();
+
+               if (stories != null) {
+                       for (MetaData meta : stories) {
+                               listKeys.add(meta);
+                               listItems.add(desc(meta));
+                       }
+               }
+
+               list = addList(listItems, 0, 0, getWidth(), getHeight(), new TAction() {
+                       @Override
+                       public void DO() {
+                               if (list.getSelectedIndex() >= 0) {
+                                       enterOnStory(listKeys.get(list.getSelectedIndex()));
+                               }
+                       }
+               });
+
+               if (false) {
+                       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 enterOnStory(MetaData meta) {
+               reader.open(meta);
+       }
+
+       private String desc(MetaData meta) {
+               return String.format("%5s: %s", meta.getLuid(), meta.getTitle());
+       }
+}
diff --git a/src/be/nikiroo/fanfix/reader/TuiReaderStoryWindow.java b/src/be/nikiroo/fanfix/reader/TuiReaderStoryWindow.java
new file mode 100644 (file)
index 0000000..47a644e
--- /dev/null
@@ -0,0 +1,60 @@
+package be.nikiroo.fanfix.reader;
+
+import jexer.TApplication;
+import jexer.TText;
+import jexer.TWindow;
+import jexer.event.TResizeEvent;
+import be.nikiroo.fanfix.Instance;
+import be.nikiroo.fanfix.data.Chapter;
+import be.nikiroo.fanfix.data.MetaData;
+import be.nikiroo.fanfix.data.Paragraph;
+import be.nikiroo.fanfix.data.Story;
+
+public class TuiReaderStoryWindow extends TWindow {
+       private MetaData meta;
+       private Story story;
+       private TText textField;
+
+       public TuiReaderStoryWindow(TApplication app, MetaData meta) {
+               super(app, desc(meta), 0, 0, 60, 18, CENTERED | RESIZABLE);
+               this.meta = meta;
+
+               // /TODO: status bar with info, buttons to change chapter << < Chapter 0
+               // : xxx.. > >> (max size for name = getWith() - X)
+
+               // TODO: show all meta info before
+
+               Chapter resume = getStory().getMeta().getResume();
+               StringBuilder text = new StringBuilder();
+               if (resume != null) {
+                       // TODO: why does \n not work but \n\n do? bug in jexer?
+                       text.append("Resume:\n\n  "); // -> to status bar
+                       for (Paragraph para : resume) {
+                               text.append(para.getContent()).append("\n\n  ");
+                       }
+               }
+               textField = addText(text.toString(), 0, 0, getWidth(), getHeight());
+       }
+
+       @Override
+       public void onResize(TResizeEvent resize) {
+               super.onResize(resize);
+
+               // Resize the text field
+               textField.setWidth(resize.getWidth());
+               textField.setHeight(resize.getHeight());
+               textField.reflow();
+       }
+
+       private Story getStory() {
+               if (story == null) {
+                       // TODO: progress bar
+                       story = Instance.getLibrary().getStory(meta.getLuid(), null);
+               }
+               return story;
+       }
+
+       private static String desc(MetaData meta) {
+               return String.format("%s: %s", meta.getLuid(), meta.getTitle());
+       }
+}