Initial version from fanfix
authorNiki Roo <niki@nikiroo.be>
Tue, 5 May 2020 11:35:46 +0000 (13:35 +0200)
committerNiki Roo <niki@nikiroo.be>
Tue, 5 May 2020 11:35:46 +0000 (13:35 +0200)
30 files changed:
LICENSE
Makefile.base [new file with mode: 0644]
README-fr.md [new file with mode: 0644]
README.md
VERSION [new file with mode: 0644]
changelog-fr.md [new file with mode: 0644]
changelog.md [new file with mode: 0644]
configure.sh [new file with mode: 0755]
libs/JSON-java-20190722-sources.jar [new file with mode: 0644]
libs/jsoup-1.10.3-sources.jar [new file with mode: 0644]
libs/licenses/JSON-java-20190722_LICENSE.txt [new file with mode: 0644]
libs/licenses/jexer-0.0.4_LICENSE.txt [new file with mode: 0644]
libs/licenses/jexer-0.0.4_README.md [new file with mode: 0644]
libs/licenses/unbescape-1.1.4_ChangeLog.txt [new file with mode: 0644]
libs/licenses/unbescape-1.1.4_LICENSE.txt [new file with mode: 0644]
libs/subtree.txt [new file with mode: 0755]
libs/unbescape-1.1.4-sources.jar [new file with mode: 0644]
libs/unbescape-1.1.4.jar [new file with mode: 0644]
screenshots/README-fr.md [new file with mode: 0644]
screenshots/README.md [new file with mode: 0644]
src/be/nikiroo/fanfix_jexer/Main.java [new file with mode: 0644]
src/be/nikiroo/jexer/TBrowsableWidget.java [new file with mode: 0644]
src/be/nikiroo/jexer/TSizeConstraint.java [new file with mode: 0644]
src/be/nikiroo/jexer/TTable.java [new file with mode: 0644]
src/be/nikiroo/jexer/TTableCellRenderer.java [new file with mode: 0644]
src/be/nikiroo/jexer/TTableCellRendererText.java [new file with mode: 0644]
src/be/nikiroo/jexer/TTableCellRendererWidget.java [new file with mode: 0644]
src/be/nikiroo/jexer/TTableColumn.java [new file with mode: 0644]
src/be/nikiroo/jexer/TTableLine.java [new file with mode: 0644]
src/be/nikiroo/jexer/TTableModel.java [new file with mode: 0644]

diff --git a/LICENSE b/LICENSE
index f288702d2fa16d3cdf0035b15a9fcbc552cd88e7..9cecc1d4669ee8af2ca727a5d8cde10cd8b2d7cc 100644 (file)
--- a/LICENSE
+++ b/LICENSE
@@ -1,7 +1,7 @@
                     GNU GENERAL PUBLIC LICENSE
                        Version 3, 29 June 2007
 
- Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
  Everyone is permitted to copy and distribute verbatim copies
  of this license document, but changing it is not allowed.
 
@@ -631,8 +631,8 @@ to attach them to the start of each source file to most effectively
 state the exclusion of warranty; and each file should have at least
 the "copyright" line and a pointer to where the full notice is found.
 
-    <one line to give the program's name and a brief idea of what it does.>
-    Copyright (C) <year>  <name of author>
+    {one line to give the program's name and a brief idea of what it does.}
+    Copyright (C) {year}  {name of author}
 
     This program is free software: you can redistribute it and/or modify
     it under the terms of the GNU General Public License as published by
@@ -645,14 +645,14 @@ the "copyright" line and a pointer to where the full notice is found.
     GNU General Public License for more details.
 
     You should have received a copy of the GNU General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 Also add information on how to contact you by electronic and paper mail.
 
   If the program does terminal interaction, make it output a short
 notice like this when it starts in an interactive mode:
 
-    <program>  Copyright (C) <year>  <name of author>
+    {project}  Copyright (C) {year}  {fullname}
     This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
     This is free software, and you are welcome to redistribute it
     under certain conditions; type `show c' for details.
@@ -664,11 +664,11 @@ might be different; for a GUI interface, you would use an "about box".
   You should also get your employer (if you work as a programmer) or school,
 if any, to sign a "copyright disclaimer" for the program, if necessary.
 For more information on this, and how to apply and follow the GNU GPL, see
-<https://www.gnu.org/licenses/>.
+<http://www.gnu.org/licenses/>.
 
   The GNU General Public License does not permit incorporating your program
 into proprietary programs.  If your program is a subroutine library, you
 may consider it more useful to permit linking proprietary applications with
 the library.  If this is what you want to do, use the GNU Lesser General
 Public License instead of this License.  But first, please read
-<https://www.gnu.org/licenses/why-not-lgpl.html>.
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/Makefile.base b/Makefile.base
new file mode 100644 (file)
index 0000000..0d365b8
--- /dev/null
@@ -0,0 +1,243 @@
+# Makefile base template
+# 
+# Version:
+# - 1.0.0: add a version comment
+# - 1.1.0: add 'help', 'sjar'
+# - 1.2.0: add 'apk'
+# - 1.2.1: improve 'apk' and add 'android'
+# - 1.3.0: add 'man' for man(ual) pages
+# - 1.4.0: remove android stuff (not working anyway)
+# - 1.5.0: include sources and readme/changelog in jar
+# - 1.5.1: include binaries from libs/bin/ into the jar
+
+# Required parameters (the commented out ones are supposed to be per project):
+
+#MAIN = path to main java source to compile
+#MORE = path to supplementary needed resources not linked from MAIN
+#NAME = name of project (used for jar output file)
+#PREFIX = usually /usr/local (where to install the program)
+#TEST = path to main test source to compile
+#JAR_FLAGS += a list of things to pack, each usually prefixed with "-C bin/"
+#SJAR_FLAGS += a list of things to pack, each usually prefixed with "-C src/",
+#              for *-sources.jar files
+#TEST_PARAMS = any parameter to pass to the test runnable when "test-run"
+
+JAVAC = javac
+JAVAC_FLAGS += -encoding UTF-8 -d ./bin/ -cp ./src/
+JAVA = java
+JAVA_FLAGS += -cp ./bin/
+JAR = jar
+RJAR = java
+RJAR_FLAGS += -jar
+
+all: build jar man
+
+help:
+       @echo "Usual options:"
+       @echo "=============="
+       @echo " make            : to build the jar file and man pages IF possible"
+       @echo " make help       : to get this help screen"
+       @echo " make libs       : to update the libraries into src/"
+       @echo " make build      : to update the binaries (not the jar)"
+       @echo " make test       : to update the test binaries"
+       @echo " make build jar  : to update the binaries and jar file"
+       @echo " make sjar       : to create the sources jar file"
+       @echo " make clean      : to clean the directory of intermediate files"
+       @echo " make mrpropre   : to clean the directory of all outputs"
+       @echo " make run        : to run the program from the binaries"
+       @echo " make run-test   : to run the test program from the binaries"
+       @echo " make jrun       : to run the program from the jar file"
+       @echo " make install    : to install the application into $$PREFIX"
+       @echo " make ifman      : to make the manual pages (if pandoc is found)"
+       @echo " make man        : to make the manual pages (requires pandoc)"
+
+.PHONY: all clean mrproper mrpropre build run jrun jar sjar resources test-resources install libs ifman man love
+
+bin:
+       @mkdir -p bin
+
+jar: $(NAME).jar
+
+sjar: $(NAME)-sources.jar
+
+build: resources
+       @echo Compiling program...
+       @echo " src/$(MAIN)"
+       @$(JAVAC) $(JAVAC_FLAGS) "src/$(MAIN).java"
+       @[ "$(MORE)" = "" ] || for sup in $(MORE); do \
+               echo "  src/$$sup" ;\
+               $(JAVAC) $(JAVAC_FLAGS) "src/$$sup.java" ; \
+       done
+
+test: test-resources
+       @[ -e bin/$(MAIN).class ] || echo You need to build the sources
+       @[ -e bin/$(MAIN).class ]
+       @echo Compiling test program...
+       @[ "$(TEST)" != "" ] || echo No test sources defined.
+       @[ "$(TEST)"  = "" ] || for sup in $(TEST); do \
+               echo "  src/$$sup" ;\
+               $(JAVAC) $(JAVAC_FLAGS) "src/$$sup.java" ; \
+       done
+
+clean:
+       rm -rf bin/
+       @echo Removing sources taken from libs...
+       @for lib in libs/*-sources.jar libs/*-sources.patch.jar; do \
+               if [ "$$lib" != 'libs/*-sources.jar' -a "$$lib" != 'libs/*-sources.patch.jar' ]; then \
+                       basename "$$lib"; \
+                       jar tf "$$lib" | while read -r ln; do \
+                               [ -f "src/$$ln" ] && rm "src/$$ln"; \
+                       done; \
+                       jar tf "$$lib" | tac | while read -r ln; do \
+                               [ -d "src/$$ln" ] && rmdir "src/$$ln" 2>/dev/null || true; \
+                       done; \
+               fi \
+       done
+
+mrproper: mrpropre
+
+mrpropre: clean
+       rm -f $(NAME).jar
+       rm -f $(NAME)-sources.jar
+       rm -f $(NAME).apk
+       rm -f $(NAME)-debug.apk
+       [ ! -e VERSION ] || rm -f "$(NAME)-`cat VERSION`.jar"
+       [ ! -e VERSION ] || rm -f "$(NAME)-`cat VERSION`-sources.jar"
+
+love:
+       @echo " ...not war."
+
+resources: libs
+       @echo Copying resources into bin/...
+       @cd src && find . | grep -v '\.java$$' | grep -v '/test/' | while read -r ln; do \
+               if [ -f "$$ln" ]; then \
+                       dir="`dirname "$$ln"`"; \
+                       mkdir -p "../bin/$$dir" ; \
+                       cp "$$ln" "../bin/$$ln" ; \
+               fi ; \
+       done
+       @cp VERSION bin/
+
+test-resources: resources
+       @echo Copying test resources into bin/...
+       @cd src && find . | grep -v '\.java$$' | grep '/test/' | while read -r ln; do \
+               if [ -f "$$ln" ]; then \
+                       dir="`dirname "$$ln"`"; \
+                       mkdir -p "../bin/$$dir" ; \
+                       cp "$$ln" "../bin/$$ln" ; \
+               fi ; \
+       done
+
+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 ../libs/*-sources.patch.jar; do \
+               if [ "$$lib" != '../libs/*-sources.jar' -a "$$lib" != '../libs/*-sources.patch.jar' ]; then \
+                       basename "$$lib"; \
+                       jar xf "$$lib"; \
+               fi \
+       done )
+       @[ ! -d libs ] || touch bin/libs
+
+$(NAME)-sources.jar: libs
+       @ls *.md >/dev/null || cp VERSION README.md
+       @echo Making sources JAR file...
+       @echo > bin/manifest
+       @[ "$(SJAR_FLAGS)" != "" ] || echo No sources JAR file defined, skipping
+       @[ "$(SJAR_FLAGS)"  = "" ] || echo Creating $(NAME)-sources.jar...
+       @[ "$(SJAR_FLAGS)"  = "" ] || $(JAR) cfm $(NAME)-sources.jar bin/manifest -C ./ *.md $(SJAR_FLAGS)
+       @[ "$(SJAR_FLAGS)"  = "" ] || [ ! -e VERSION ] || echo Copying to "$(NAME)-`cat VERSION`-sources.jar"...
+       @[ "$(SJAR_FLAGS)"  = "" ] || [ ! -e VERSION ] || cp $(NAME)-sources.jar "$(NAME)-`cat VERSION`-sources.jar"
+
+$(NAME).jar: resources
+       @[ -e bin/$(MAIN).class ] || echo You need to build the sources
+       @[ -e bin/$(MAIN).class ]
+       @ls *.md >/dev/null || cp VERSION README.md
+       @echo "Copying documentation into bin/..."
+       @cp -r *.md bin/ || cp VERSION bin/no-documentation.md
+       @[ ! -d libs/bin/ ] || echo "Copying additional binaries from libs/bin/ into bin/..."
+       @[ ! -d libs/bin/ ] || cp -r libs/bin/* bin/
+       @echo "Copying sources into bin/..."
+       @cp -r src/* bin/
+       @echo "Making jar..."
+       @echo "Main-Class: `echo "$(MAIN)" | sed 's:/:.:g'`" > bin/manifest
+       @echo >> bin/manifest
+       $(JAR) cfm $(NAME).jar bin/manifest -C ./ *.md $(JAR_FLAGS)
+       @[ ! -e VERSION ] || echo Copying to "$(NAME)-`cat VERSION`.jar"...
+       @[ ! -e VERSION ] || cp $(NAME).jar "$(NAME)-`cat VERSION`.jar"
+
+run: 
+       @[ -e bin/$(MAIN).class ] || echo You need to build the sources
+       @[ -e bin/$(MAIN).class ]
+       @echo Running "$(NAME)"...
+       $(JAVA) $(JAVA_FLAGS) $(MAIN)
+
+jrun:
+       @[ -e $(NAME).jar ] || echo You need to build the jar
+       @[ -e $(NAME).jar ]
+       @echo Running "$(NAME).jar"...
+       $(RJAR) $(RJAR_FLAGS) $(NAME).jar
+
+run-test: 
+       @[ "$(TEST)" = "" -o -e "bin/$(TEST).class" ] || echo You need to build the test sources
+       @[ "$(TEST)" = "" -o -e "bin/$(TEST).class" ]
+       @echo Running tests for "$(NAME)"...
+       @[ "$(TEST)" != "" ] || echo No test sources defined.
+       [ "$(TEST)"  = "" ] || ( clear ; $(JAVA) $(JAVA_FLAGS) $(TEST) $(TEST_PARAMS) )
+
+install:
+       @[ -e $(NAME).jar ] || echo You need to build the jar
+       @[ -e $(NAME).jar ]
+       mkdir -p "$(PREFIX)/lib" "$(PREFIX)/bin"
+       cp $(NAME).jar "$(PREFIX)/lib/"
+       echo "#!/bin/sh" > "$(PREFIX)/bin/$(NAME)"
+       echo "$(RJAR) $(RJAR_FLAGS) \"$(PREFIX)/lib/$(NAME).jar\" \"\$$@\"" >> "$(PREFIX)/bin/$(NAME)"
+       chmod a+rx "$(PREFIX)/bin/$(NAME)"
+       if [ -e "man/man1/$(NAME).1" ]; then \
+               cp -r man/ "$(PREFIX)"/share/; \
+       fi
+
+ifman:
+       @if pandoc -v >/dev/null 2>&1; then \
+               make man; \
+       else \
+               echo "man pages not generated: "'`'"pandoc' required"; \
+       fi
+
+man: 
+       @echo Checking for possible manual pages...
+       @if [ -e README.md ]; then \
+               echo Sources found for man pages; \
+               if pandoc -v >/dev/null 2>&1; then \
+                       ls README*.md 2>/dev/null \
+                                       | grep 'README\(-..\|\)\.md' \
+                                       | while read man; do \
+                               echo "  Processing page $$lang..."; \
+                               lang="`echo "$$man" \
+                                       | sed 's:README\.md:en:' \
+                                       | sed 's:README-\(.*\)\.md:\1:'`"; \
+                               mkdir -p man/"$$lang"/man1; \
+                               ( \
+                                       echo ".TH \"${NAME}\" 1 `\
+                                               date +%Y-%m-%d\
+                                               ` \"version `cat VERSION`\""; \
+                                       echo; \
+                                       UNAME="`echo "${NAME}" \
+                                               | sed 's:\(.*\):\U\1:g'`"; \
+                                       ( \
+                                               cat "$$man" | head -n1 \
+       | sed 's:.*(README\(-fr\|\)\.md).*::g'; \
+                                               cat "$$man" | tail -n+2; \
+                                       ) | sed 's:^#\(#.*\):\1:g' \
+       | sed 's:^\(#.*\):\U\1:g;s:# *'"$$UNAME"':# NAME\n'"${NAME}"' \\- :g' \
+       | sed 's:--:——:g' \
+       | pandoc -f markdown -t man | sed 's:——:--:g' ; \
+                               ) > man/"$$lang"/man1/"${NAME}.1"; \
+                       done; \
+                       mkdir -p "man/man1"; \
+                       cp man/en/man1/"${NAME}".1 man/man1/; \
+               else \
+                       echo "man pages generation: pandoc required" >&2; \
+                       false; \
+               fi; \
+       fi;
+
diff --git a/README-fr.md b/README-fr.md
new file mode 100644 (file)
index 0000000..9fbafd9
--- /dev/null
@@ -0,0 +1,111 @@
+[English](README.md) Français
+
+# Fanfix-jexer
+
+Fanfix-jexer est un programme qui offre une interface texte (via la librairie Jexer) Ã  la librairie de téléchargement de comics/histoires/mangas [Fanfix](https://github.com/nikiroo/fanfix).
+
+## Synopsis
+
+- ```fanfix-jexer```
+- ```fanfix-jexer [...]``` (options [Fanfix](https://github.com/nikiroo/fanfix))
+
+## Description
+
+(Si vous voulez juste voir les derniers changements, vous pouvez regarder le [Changelog](changelog-fr.md) -- remarquez que le programme affiche le changelog si une version plus récente est détectée depuis la version x.x.x.)
+
+![Main GUI](screenshots/fanfix-jexer.png?raw=true "Fenêtre principale")
+
+Une gallerie de screenshots est disponible [ici](screenshots/README-fr.md).
+
+Le fonctionnement du programme est assez simple : il converti une URL venant d'un site supporté en un fichier .epub pour les histoires ou .cbz pour les comics (d'autres options d'enregistrement sont disponibles, comme du texte simple, du HTML...).
+
+Pour vous aider Ã  organiser vos histoires, il peut aussi servir de bibliothèque locale vous permettant :
+
+- d'importer une histoire depuis son URL (ou depuis un fichier)
+- d'exporter une histoire dans un des formats supportés vers un fichier
+- d'afficher une histoire **nativement** ou **en appelant un programme natif pour lire le fichier**
+
+### Sites supportés
+
+Pour le moment, les sites suivants sont supportés :
+
+- http://FimFiction.net/ : fanfictions dévouées Ã  la série My Little Pony
+- http://Fanfiction.net/ : fanfictions venant d'une multitude d'univers différents, depuis les shows télévisés aux livres en passant par les jeux-vidéos
+- http://mangahub.io/ : un site répertoriant une quantité non négligeable de mangas (English)
+- https://e621.net/ : un site Furry proposant des comics, y compris de MLP
+- https://sofurry.com/ : même chose, mais orienté sur les histoires plutôt que les images
+- https://e-hentai.org/ : support ajouté sur demande : n'hésitez pas Ã  demander un site !
+- http://mangas-lecture-en-ligne.fr/ : un site proposant beaucoup de mangas, en français
+
+### Types de fichiers supportés
+
+Nous supportons les types de fichiers suivants (aussi bien en entrée qu'en sortie) :
+
+- epub : les fichiers .epub créés avec Fanfix (nous ne supportons pas les autres fichiers .epub, du moins pour le moment)
+- text : les histoires enregistrées en texte (.txt), avec quelques règles spécifiques :
+       - le titre doit Ãªtre sur la première ligne
+       - l'auteur (précédé de rien, ```Par ```, ```De ``` ou ```©```) doit Ãªtre sur la deuxième ligne, optionnellement suivi de la date de publication entre parenthèses (i.e., ```Par Quelqu'un (3 octobre 1998)```)
+       - les chapitres doivent Ãªtre déclarés avec ```Chapitre x``` ou ```Chapitre x: NOM DU CHAPTITRE```, où ```x``` est le numéro du chapitre
+       - une description de l'histoire doit Ãªtre donnée en tant que chaptire 0
+       - une image de couverture peut Ãªtre présente avec le même nom de fichier que l'histoire, mais une extension .png, .jpeg ou .jpg
+- info_text : fort proche du format texte, mais avec un fichier .info accompagnant l'histoire pour y enregistrer quelques metadata (le fichier de metadata est supposé Ãªtre créé par Fanfix, ou Ãªtre compatible avec)
+- cbz : les fichiers .cbz (une collection d'images zipées), de préférence créés avec Fanfix (même si les autres .cbz sont aussi supportés, mais sans la majorité des metadata de Fanfix dans ce cas)
+- html : les fichiers HTML que vous pouvez ouvrir avec n'importe quel navigateur ; remarquez que Fanfix créera un répertoire pour y mettre les fichiers nécessaires, dont un fichier ```index.html``` pour afficher le tout -- nous ne supportons en entrée que les fichiers HTML créés par Fanfix
+
+### Plateformes supportées
+
+Toute plateforme supportant Java 1.6 devrait suffire.
+
+Le programme a Ã©té testé sur Linux (Debian, Slackware et Ubuntu), MacOS X et Windows pour le moment, mais n'hésitez pas Ã  nous informer si vous l'essayez sur un autre système.
+
+Si vous avez des difficultés pour le compiler avec une version supportée de Java (1.6+), contactez-nous.
+
+Note pour Windows : nous proposons aussi un laucnher exécutable au format EXE qui vérifie si Java est disponible avant de lancer le programme, et explique comment l'installer si pas.
+
+## Options
+
+Vous pouvez démarrer le programme sans paramètres :
+
+- ```java -jar fanfix-jexer.jar```
+- ```fanfix-jexer``` (si vous avez utilisé *make install*)
+
+Vous pouvez aussi utiliser les options que [Fanfix](https://github.com/nikiroo/fanfix) supporte.
+
+### Environnement
+
+Certaines variables d'environnement sont reconnues par le programme :
+
+- ```LANG=en```: forcer la langue du programme en anglais
+- ```CONFIG_DIR=$HOME/.fanfix```: utilise ce répertoire pour les fichiers de configuration du programme (et copie les fichiers de configuration par défaut si besoin)
+- ```NOUTF=1```: essaye d'utiliser des caractères non-unicode quand possible (cela peut avoir un impact sur les fichiers générés, pas uniquement sur les messages Ã  l'utilisateur)
+- ```DEBUG=1```: force l'option ```DEBUG=true``` du fichier de configuration (pour afficher plus d'information en cas d'erreur)
+
+## Compilation
+
+```./configure.sh && make```
+
+Vous pouvez aussi importer les sources java dans, par exemple, [Eclipse](https://eclipse.org/), et faire un JAR exécutable depuis celui-ci.
+
+### Librairies dépendantes (incluses)
+
+Nécessaires :
+
+- ```src/be/nikiroo/utils```: quelques utilitaires partagés, inclus en tant que subtree
+- ```src/be/nikiroo/fanfix```: la librairie Fanfix sur laquelle tout le programme est basé, inclus en tant que subtree
+- ```src/be/nikiroo/jexer```: la librairie jexer avec quelques modification, inclus en tant que subtree
+- [```libs/unbescape-sources.jar```](https://github.com/unbescape/unbescape): une librairie sympathique pour convertir du texte depuis/vers beaucoup de formats ; utilisée ici pour la partie HTML
+- [```libs/jsoup-sources.jar```](https://jsoup.org/): une libraririe pour parser du HTML
+- [```libs/JSON-java-20190722-sources.jar```](https://github.com/stleary/JSON-java): une libraririe pour parser du JSON
+
+Optionnelles :
+
+- [```pandoc```](http://pandoc.org/): pour générer les man pages depuis les fichiers README, non inclu (pour l'utiliser, il faut que le programme ```pandoc``` soit disponible dans le PATH)
+
+Rien d'autre, si ce n'est Java 1.6+.
+
+À noter : ```make libs``` exporte ces librairies dans le répertoire src/.
+
+## Auteur
+
+Fanfix a Ã©té Ã©crit par Niki Roo <niki@nikiroo.be>
+
index 629633a9087475d8aafc451fb9a2a4205a95f67a..92f2e2dc6d65e69792bff34184172294d6494df8 100644 (file)
--- a/README.md
+++ b/README.md
@@ -1,2 +1,111 @@
-# fanfix-jexer
-A TUI interface with the library Jexer for fanfix
+English [Français](README-fr.md)
+
+# Fanfix-jexer
+
+Fanfix-jexer is a program that offer you a text interface (via the Jexer librairy) around the comics/stories/mangas library [Fanfix](https://github.com/nikiroo/fanfix).
+
+## Synopsis
+
+- ```fanfix-jexer```
+- ```fanfix-jexer [...]``` ([Fanfix](https://github.com/nikiroo/fanfix) options)
+
+## Description
+
+(If you are interested in the recent changes, please check the [Changelog](changelog.md) -- note that starting from version x.x.x, the changelog is checked at startup.)
+
+![Main GUI](screenshots/fanfix-jexer.png?raw=true "Main window")
+
+A screenshots gallery can be found [here](screenshots/README.md).
+
+It will convert from a (supported) URL to an .epub file for stories or a .cbz file for comics (a few other output types are also available, like Plain Text, LaTeX, HTML...).
+
+To help organize your stories, it can also work as a local library so you can:
+
+- Import a story from its URL (or just from a file)
+- Export a story to a file (in any of the supported output types)
+- Display a story from the local library **natively** or **by calling a native program to handle it**
+
+### Supported websites
+
+Currently, the following websites are supported:
+
+- http://FimFiction.net/: fan fictions devoted to the My Little Pony show
+- http://Fanfiction.net/: fan fictions of many, many different universes, from TV shows to novels to games
+- http://mangahub.io/: a well filled repository of mangas (English)
+- https://e621.net/: a Furry website supporting comics, including MLP
+- https://sofurry.com/: same thing, but story-oriented
+- https://e-hentai.org/: done upon request (so, feel free to ask for more websites!)
+- http://mangas-lecture-en-ligne.fr/: a website offering a lot of mangas (in French)
+
+### Support file types
+
+We support a few file types for local story conversion (both as input and as output):
+
+- epub: .epub files created by this program (we do not support "all" .epub files, at least for now)
+- text: local stories encoded in plain text format, with a few specific rules:
+       - the title must be on the first line
+       - the author (preceded by nothing, ```by ``` or ```©```) must be on the second line, possibly with the publication date in parenthesis (i.e., ```By Unknown (3rd October 1998)```)
+       - chapters must be declared with ```Chapter x``` or ```Chapter x: NAME OF THE CHAPTER```, where ```x``` is the chapter number
+       - a description of the story must be given as chapter number 0
+       - a cover image may be present with the same filename as the story, but a .png, .jpeg or .jpg extension
+- info_text: contains the same information as the text format, but with a companion .info file to store some metadata (the .info file is supposed to be created by Fanfix or compatible with it)
+- cbz: .cbz (collection of images) files, preferably created with Fanfix (but any .cbz file is supported, though without most of Fanfix metadata, obviously)
+- html: HTML files that you can open with any browser; note that it will create a directory structure with ```index.html``` as the main file -- we only support importing HTML files created by Fanfix
+
+### Supported platforms
+
+Any platform with at lest Java 1.6 on it should be ok.
+
+It has been tested on Linux (Debian, Slackware, Ubuntu), MacOS X and Windows for now, but feel free to inform us if you try it on another system.
+
+If you have any problems to compile it with a supported Java version (1.6+), please contact us.
+
+Note for Windows : we also offer a launcher in EXE format that checks if Java is available before starting the program, and helps you install it if not.
+
+## Options
+
+You can start the program without parameters:
+
+- ```java -jar fanfix-jexer.jar```
+- ```fanfix-jexer``` (if you used *make install*)
+
+You can also use the options supported by [Fanfix](https://github.com/nikiroo/fanfix).
+
+### Environment
+
+Some environment variables are recognized by the program:
+
+- ```LANG=en```: force the language to English
+- ```CONFIG_DIR=$HOME/.fanfix```: use the given directory as a config directory (and copy the default configuration if needed)
+- ```NOUTF=1```: try to fallback to non-unicode values when possible (can have an impact on the resulting files, not only on user messages)
+- ```DEBUG=1```: force the ```DEBUG=true``` option of the configuration file (to show more information on errors)
+
+## Compilation
+
+```./configure.sh && make```
+
+You can also import the java sources into, say, [Eclipse](https://eclipse.org/), and create a runnable JAR file from there.
+
+### Dependant libraries (included)
+
+Required:
+
+- ```src/be/nikiroo/utils```: some shared utility functions, included as a subtree
+- ```src/be/nikiroo/fanfix```: the fanfix library upon which all the program is based, included as a subtree
+- ```src/be/nikiroo/jexer```: the jexer library with some custom changes, included as a subtree
+- [```libs/unbescape-sources.jar```](https://github.com/unbescape/unbescape): a nice library to escape/unescape a lot of text formats; used here for HTML
+- [```libs/jsoup-sources.jar```](https://jsoup.org/): a library to parse HTML
+- [```libs/JSON-java-20190722-sources.jar```](https://github.com/stleary/JSON-java): a library to parse JSON
+
+Optional:
+
+- [```pandoc```](http://pandoc.org/): to generate the man pages from the README files (to use it, ```pandoc``` must be available as program in the PATH)
+
+Nothing else but Java 1.6+.
+
+Note that calling ```make libs``` will export the libraries into the src/ directory.
+
+## Author
+
+Fanfix was written by Niki Roo <niki@nikiroo.be>
+
diff --git a/VERSION b/VERSION
new file mode 100644 (file)
index 0000000..8acdd82
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+0.0.1
diff --git a/changelog-fr.md b/changelog-fr.md
new file mode 100644 (file)
index 0000000..0773a86
--- /dev/null
@@ -0,0 +1,6 @@
+# Fanfix-jexer
+
+# Version WIP
+
+- new: version initiale, reprise de [fanfix](https://github.com/nikiroo/fanfix)
+
diff --git a/changelog.md b/changelog.md
new file mode 100644 (file)
index 0000000..621db60
--- /dev/null
@@ -0,0 +1,6 @@
+# Fanfix-jexer
+
+# Version WIP
+
+- new: initial version, copied from [fanfix](https://github.com/nikiroo/fanfix)
+
diff --git a/configure.sh b/configure.sh
new file mode 100755 (executable)
index 0000000..7c94583
--- /dev/null
@@ -0,0 +1,70 @@
+#!/bin/sh
+
+# default:
+PREFIX=/usr/local
+PROGS="java javac jar make sed"
+
+valid=true
+while [ "$*" != "" ]; do
+       key=`echo "$1" | cut -f1 -d=`
+       val=`echo "$1" | cut -f2 -d=`
+       case "$key" in
+       --)
+       ;;
+       --help) #               This help message
+               echo The following arguments can be used:
+               cat "$0" | grep '^\s*--' | grep '#' | while read ln; do
+                       cmd=`echo "$ln" | cut -f1 -d')'`
+                       msg=`echo "$ln" | cut -f2 -d'#'`
+                       echo "  $cmd$msg"
+               done
+       ;;
+       --prefix) #=PATH        Change the prefix to the given path
+               PREFIX="$val"
+       ;;
+       *)
+               echo "Unsupported parameter: '$1'" >&2
+               echo >&2
+               sh "$0" --help >&2
+               valid=false
+       ;;
+       esac
+       shift
+done
+
+[ $valid = false ] && exit 1
+
+MESS="A required program cannot be found:"
+for prog in $PROGS; do
+       out="`whereis -b "$prog" 2>/dev/null`"
+       if [ "$out" = "$prog:" ]; then
+               echo "$MESS $prog" >&2
+               valid=false
+       fi
+done
+
+[ $valid = false ] && exit 2
+
+if [ "`whereis tput`" = "tput:" ]; then
+       ok='"[ ok ]"';
+       ko='"[ !! ]"';
+       cols=80;
+else
+       #ok='"`tput bold`[`tput setf 2` OK `tput init``tput bold`]`tput init`"';
+       #ko='"`tput bold`[`tput setf 4` !! `tput init``tput bold`]`tput init`"';
+       ok='"`tput bold`[`tput setaf 2` OK `tput init``tput bold`]`tput init`"';
+       ko='"`tput bold`[`tput setaf 1` !! `tput init``tput bold`]`tput init`"';
+       cols='"`tput cols`"';
+fi;
+
+echo "MAIN = be/nikiroo/fanfix_jexer/Main" > Makefile
+echo "MORE = " >> Makefile
+echo "TEST = " >> Makefile
+echo "TEST_PARAMS = $cols $ok $ko" >> Makefile
+echo "NAME = fanfix-jexer" >> Makefile
+echo "PREFIX = $PREFIX" >> Makefile
+echo "JAR_FLAGS += -C bin/ org -C bin/ be -C ./ LICENSE -C ./ VERSION -C libs/ licenses" >> Makefile
+#echo "SJAR_FLAGS += -C src/ org -C src/ jexer -C src/ be -C ./ LICENSE -C ./ VERSION -C libs/ licenses" >> Makefile
+
+cat Makefile.base >> Makefile
+
diff --git a/libs/JSON-java-20190722-sources.jar b/libs/JSON-java-20190722-sources.jar
new file mode 100644 (file)
index 0000000..22a416d
Binary files /dev/null and b/libs/JSON-java-20190722-sources.jar differ
diff --git a/libs/jsoup-1.10.3-sources.jar b/libs/jsoup-1.10.3-sources.jar
new file mode 100644 (file)
index 0000000..1fe0db4
Binary files /dev/null and b/libs/jsoup-1.10.3-sources.jar differ
diff --git a/libs/licenses/JSON-java-20190722_LICENSE.txt b/libs/licenses/JSON-java-20190722_LICENSE.txt
new file mode 100644 (file)
index 0000000..02ee0ef
--- /dev/null
@@ -0,0 +1,23 @@
+============================================================================
+
+Copyright (c) 2002 JSON.org
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+The Software shall be used for Good, not Evil.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/libs/licenses/jexer-0.0.4_LICENSE.txt b/libs/licenses/jexer-0.0.4_LICENSE.txt
new file mode 100644 (file)
index 0000000..09bbfe0
--- /dev/null
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 Kevin Lamonte
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/libs/licenses/jexer-0.0.4_README.md b/libs/licenses/jexer-0.0.4_README.md
new file mode 100644 (file)
index 0000000..7cfe9b4
--- /dev/null
@@ -0,0 +1,220 @@
+Jexer - Java Text User Interface library
+========================================
+
+This library implements a text-based windowing system reminiscient of
+Borland's [Turbo Vision](http://en.wikipedia.org/wiki/Turbo_Vision)
+system.  (For those wishing to use the actual C++ Turbo Vision
+library, see [Sergio Sigala's C++ version based on the public domain
+sources released by Borland.](http://tvision.sourceforge.net/) )
+
+Jexer currently supports three backends:
+
+* System.in/out to a command-line ECMA-48 / ANSI X3.64 type terminal
+  (tested on Linux + xterm).  I/O is handled through terminal escape
+  sequences generated by the library itself: ncurses is not required
+  or linked to.  xterm mouse tracking using UTF8 and SGR coordinates
+  are supported.  For the demo application, this is the default
+  backend on non-Windows/non-Mac platforms.
+
+* The same command-line ECMA-48 / ANSI X3.64 type terminal as above,
+  but to any general InputStream/OutputStream or Reader/Writer.  See
+  the file jexer.demos.Demo2 for an example of running the demo over a
+  TCP socket.  jexer.demos.Demo3 demonstrates how one might use a
+  character encoding than the default UTF-8.
+
+* Java Swing UI.  This backend can be selected by setting
+  jexer.Swing=true.  The default window size for Swing is 80x25, which
+  is set in jexer.session.SwingSession.  For the demo application,
+  this is the default backend on Windows and Mac platforms.
+
+Additional backends can be created by subclassing
+jexer.backend.Backend and passing it into the TApplication
+constructor.
+
+The Jexer homepage, which includes additional information and binary
+release downloads, is at: https://jexer.sourceforge.io .  The Jexer
+source code is hosted at: https://github.com/klamonte/jexer .
+
+
+
+License
+-------
+
+This project is licensed under the MIT License.  See the file LICENSE
+for the full license text.
+
+
+
+Acknowledgements
+----------------
+
+Jexer makes use of the Terminus TrueType font [made available
+here](http://files.ax86.net/terminus-ttf/) .
+
+
+
+Usage
+-----
+
+Simply subclass TApplication and then run it in a new thread:
+
+```Java
+import jexer.*;
+
+class MyApplication extends TApplication {
+
+    public MyApplication() throws Exception {
+        super(BackendType.SWING); // Could also use BackendType.XTERM
+
+        // Create standard menus for File and Window
+        addFileMenu();
+        addWindowMenu();
+
+        // Add a custom window, see below for its code.
+        addWindow(new MyWindow(this));
+    }
+
+    public static void main(String [] args) {
+        try {
+            MyApplication app = new MyApplication();
+            (new Thread(app)).start();
+        } catch (Throwable t) {
+            t.printStackTrace();
+        }
+    }
+}
+```
+
+Similarly, subclass TWindow and add some widgets:
+
+```Java
+class MyWindow extends TWindow {
+
+    public MyWindow(TApplication application) {
+        // See TWindow's API for several constructors.  This one uses the
+        // application, title, width, and height.  Note that the window width
+        // and height include the borders.  The widgets inside the window
+        // will see (0, 0) as the top-left corner inside the borders,
+        // i.e. what the window would see as (1, 1).
+        super(application, "My Window", 30, 20);
+
+        // See TWidget's API for convenience methods to add various kinds of
+        // widgets.  Note that ANY widget can be a container for other
+        // widgets: TRadioGroup for example has TRadioButtons as child
+        // widgets.
+
+        // We will add a basic label, text entry field, and button.
+        addLabel("This is a label", 5, 3);
+        addField(5, 5, 20, false, "enter text here");
+        // For the button, we will pop up a message box if the user presses
+        // it.
+        addButton("Press &Me!", 5, 8, new TAction() {
+            public void DO() {
+                MyWindow.this.messageBox("Box Title", "You pressed me, yay!");
+            }
+        } );
+    }
+}
+```
+
+Put these into a file, compile it with jexer.jar in the classpath, run
+it and you'll see an application like this:
+
+![The Example Code Above](/screenshots/readme_application.png?raw=true "The application in the text of README.md")
+
+See the files in jexer.demos for many more detailed examples showing
+all of the existing UI controls.  The demo can be run in three
+different ways:
+
+  * 'java -jar jexer.jar' .  This will use System.in/out with
+    xterm-like sequences on non-Windows platforms.  On Windows it will
+    use a Swing JFrame.
+
+  * 'java -Djexer.Swing=true -jar jexer.jar' .  This will always use
+    Swing on any platform.
+
+  * 'java -cp jexer.jar jexer.demos.Demo2 PORT' (where PORT is a
+    number to run the TCP daemon on).  This will use the telnet
+    protocol to establish an 8-bit clean channel and be aware of
+    screen size changes.
+
+
+
+More Screenshots
+----------------
+
+![Several Windows Open Including A Terminal](/screenshots/screenshot1.png?raw=true "Several Windows Open Including A Terminal")
+
+![Yo Dawg...](/screenshots/yodawg.png?raw=true "Yo Dawg, I heard you like text windowing systems, so I ran a text windowing system inside your text windowing system so you can have a terminal in your terminal.")
+
+
+
+System Properties
+-----------------
+
+The following properties control features of Jexer:
+
+  jexer.Swing
+  -----------
+
+  Used only by jexer.demos.Demo1.  If true, use the Swing interface
+  for the demo application.  Default: true on Windows platforms
+  (os.name starts with "Windows"), false on non-Windows platforms.
+
+  jexer.Swing.cursorStyle
+  -----------------------
+
+  Used by jexer.io.SwingScreen.  Selects the cursor style to draw.
+  Valid values are: underline, block, outline.  Default: underline.
+
+
+
+Known Issues / Arbitrary Decisions
+----------------------------------
+
+Some arbitrary design decisions had to be made when either the
+obviously expected behavior did not happen or when a specification was
+ambiguous.  This section describes such issues.
+
+  - See jexer.tterminal.ECMA48 for more specifics of terminal
+    emulation limitations.
+
+  - TTerminalWindow uses cmd.exe on Windows.  Output will not be seen
+    until enter is pressed, due to cmd.exe's use of line-oriented
+    input (see the ENABLE_LINE_INPUT flag for GetConsoleMode() and
+    SetConsoleMode()).
+
+  - TTerminalWindow launches 'script -fqe /dev/null' or 'script -q -F
+    /dev/null' on non-Windows platforms.  This is a workaround for the
+    C library behavior of checking for a tty: script launches $SHELL
+    in a pseudo-tty.  This works on Linux and Mac but might not on
+    other Posix-y platforms.
+
+  - Closing a TTerminalWindow without exiting the process inside it
+    may result in a zombie 'script' process.
+
+  - Java's InputStreamReader as used by the ECMA48 backend requires a
+    valid UTF-8 stream.  The default X10 encoding for mouse
+    coordinates outside (160,94) can corrupt that stream, at best
+    putting garbage keyboard events in the input queue but at worst
+    causing the backend reader thread to throw an Exception and exit
+    and make the entire UI unusable.  Mouse support therefore requires
+    a terminal that can deliver either UTF-8 coordinates (1005 mode)
+    or SGR coordinates (1006 mode).  Most modern terminals can do
+    this.
+
+  - jexer.session.TTYSession calls 'stty size' once every second to
+    check the current window size, performing the same function as
+    ioctl(TIOCGWINSZ) but without requiring a native library.
+
+  - jexer.io.ECMA48Terminal calls 'stty' to perform the equivalent of
+    cfmakeraw() when using System.in/out.  System.out is also
+    (blindly!)  put in 'stty sane cooked' mode when exiting.
+
+
+
+Roadmap
+-------
+
+Many tasks remain before calling this version 1.0.  See docs/TODO.md
+for the complete list of tasks.
diff --git a/libs/licenses/unbescape-1.1.4_ChangeLog.txt b/libs/licenses/unbescape-1.1.4_ChangeLog.txt
new file mode 100644 (file)
index 0000000..9cec6ec
--- /dev/null
@@ -0,0 +1,32 @@
+1.1.4.RELEASE
+=============
+- Added ampersand (&) to the list of characters to be escaped in LEVEL 1 for JSON, JavaScript and CSS literals
+  in order to make escaped code safe against code injection attacks in XHTML scenarios (browsers using XHTML
+  processing mode) performed by means of including XHTML escape codes in literals.
+
+1.1.3.RELEASE
+=============
+- Improved performance of String-based unescape methods for HTML, XML, JS, JSON and others when the
+  text to be unescaped actually needs no unescaping.
+
+1.1.2.RELEASE
+=============
+- Added support for stream-based (String-to-Writer and Reader-to-Writer) escape and unescape operations.
+
+1.1.1.RELEASE
+=============
+- Fixed HTML unescape for codepoints > U+10FFFF (was throwing IllegalArgumentException).
+- Fixed HTML unescape for codepoints > Integer.MAX_VALUE (was throwing ArrayIndexOutOfBounds).
+- Simplified and improved performance of codepoint-computing code by using Character.codePointAt(...) instead
+  of a complex conditional structure based on Character.isHighSurrogate(...) and Character.isLowSurrogate(...).
+- [doc] Fixed description of MSExcel-compatible CSV files.
+
+
+1.1.0.RELEASE
+=============
+- Added URI/URL escape and unescape operations.
+
+
+1.0
+===
+- First release of unbescape.
diff --git a/libs/licenses/unbescape-1.1.4_LICENSE.txt b/libs/licenses/unbescape-1.1.4_LICENSE.txt
new file mode 100644 (file)
index 0000000..d645695
--- /dev/null
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/libs/subtree.txt b/libs/subtree.txt
new file mode 100755 (executable)
index 0000000..fbcbd7a
--- /dev/null
@@ -0,0 +1,20 @@
+# Subtrees
+
+# TODO: remove jexer subtree, work with an official version instead
+
+# The subtrees used for this program:
+#      ```git subtree add -P src/be/nikiroo/utils git@github.com:nikiroo/nikiroo-utils.git subtree```
+#      ```git subtree add -P src/be/nikiroo/fanfix git@github.com:nikiroo/fanfix.git subtree```
+#      ```git subtree add -P src/jexer git@github.com:nikiroo/jexer.git subtree```
+
+# Update all subtrees:
+# 
+# git subtree pull -P src/be/nikiroo/utils git@github.com:nikiroo/nikiroo-utils.git subtree
+# git subtree pull -P src/be/nikiroo/fanfix git@github.com:nikiroo/fanfix.git subtree
+# git subtree pull -P src/jexer git@github.com:nikiroo/jexer.git subtree
+
+# Push all subtrees:
+
+git subtree push -P src/be/nikiroo/utils git@github.com:nikiroo/nikiroo-utils.git subtree
+git subtree push -P src/be/nikiroo/fanfix git@github.com:nikiroo/fanfix.git subtree
+git subtree push -P src/jexer git@github.com:nikiroo/jexer.git subtree
diff --git a/libs/unbescape-1.1.4-sources.jar b/libs/unbescape-1.1.4-sources.jar
new file mode 100644 (file)
index 0000000..01ddb56
Binary files /dev/null and b/libs/unbescape-1.1.4-sources.jar differ
diff --git a/libs/unbescape-1.1.4.jar b/libs/unbescape-1.1.4.jar
new file mode 100644 (file)
index 0000000..f192048
Binary files /dev/null and b/libs/unbescape-1.1.4.jar differ
diff --git a/screenshots/README-fr.md b/screenshots/README-fr.md
new file mode 100644 (file)
index 0000000..8a95907
--- /dev/null
@@ -0,0 +1,11 @@
+# Fanfix-jexer
+
+## Screenshots
+
+Cette gallerie reprend des screenshots de plusieurs versions de Fanfix, mais les versions les plus récentes sont affichées en premier.
+
+### Version WIP
+
+La fenêtre principale du programme :
+![Fenêtre principale](fanfix-swing-?.?.?.png)
+
diff --git a/screenshots/README.md b/screenshots/README.md
new file mode 100644 (file)
index 0000000..aedd5e9
--- /dev/null
@@ -0,0 +1,11 @@
+# Fanfix
+
+## Screenshots
+
+This screenshots gallery shows screenshots of different versions of Fanfix, but shows the more recent ones on top.
+
+### Version WIP
+
+The main window of the program:
+![Fenêtre principale](fanfix-swing-?.?.?.png)
+
diff --git a/src/be/nikiroo/fanfix_jexer/Main.java b/src/be/nikiroo/fanfix_jexer/Main.java
new file mode 100644 (file)
index 0000000..eba2d2e
--- /dev/null
@@ -0,0 +1,43 @@
+package be.nikiroo.fanfix_jexer;
+
+import java.io.IOException;
+
+import be.nikiroo.fanfix.Instance;
+import be.nikiroo.fanfix.reader.Reader.ReaderType;
+import be.nikiroo.fanfix.reader.tui.TuiReader;
+
+/**
+ * The main class of the application, the launcher.
+ * 
+ * @author niki
+ */
+public class Main {
+       /**
+        * The main entry point of the application.
+        * <p>
+        * If arguments are passed, everything will be passed to Fanfix CLI; if no
+        * argument are present, Fanfix-Swing proper will be launched.
+        * 
+        * @param args
+        *            the arguments (none, or will be passed to Fanfix)
+        */
+       public static void main(String[] args) {
+               // Defer to main application if parameters (we are only a UI)
+               // (though we could handle some of the parameters in the future,
+               // maybe importing via ImporterFrame? but that would require a
+               // unique instance of the UI to be usable...)
+               if (args != null && args.length > 0) {
+                       be.nikiroo.fanfix.Main.main(args);
+                       return;
+               }
+
+               Instance.init();
+
+               TuiReader.setDefaultReaderType(ReaderType.TUI);
+               try {
+                       TuiReader.getReader().browse(null);
+               } catch (IOException e) {
+                       Instance.getInstance().getTraceHandler().error(e);
+               }
+       }
+}
diff --git a/src/be/nikiroo/jexer/TBrowsableWidget.java b/src/be/nikiroo/jexer/TBrowsableWidget.java
new file mode 100644 (file)
index 0000000..44fa710
--- /dev/null
@@ -0,0 +1,418 @@
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 David "Niki" ROULET
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author David ROULET [niki@nikiroo.be]
+ * @version 2
+ */
+package be.nikiroo.jexer;
+
+import static jexer.TKeypress.kbBackTab;
+import static jexer.TKeypress.kbDown;
+import static jexer.TKeypress.kbEnd;
+import static jexer.TKeypress.kbEnter;
+import static jexer.TKeypress.kbHome;
+import static jexer.TKeypress.kbLeft;
+import static jexer.TKeypress.kbPgDn;
+import static jexer.TKeypress.kbPgUp;
+import static jexer.TKeypress.kbRight;
+import static jexer.TKeypress.kbShiftTab;
+import static jexer.TKeypress.kbTab;
+import static jexer.TKeypress.kbUp;
+import jexer.THScroller;
+import jexer.TScrollableWidget;
+import jexer.TVScroller;
+import jexer.TWidget;
+import jexer.event.TKeypressEvent;
+import jexer.event.TMouseEvent;
+import jexer.event.TResizeEvent;
+
+/**
+ * This class represents a browsable {@link TWidget}, that is, a {@link TWidget}
+ * where you can use the keyboard or mouse to browse to one line to the next, or
+ * from left t right.
+ * 
+ * @author niki
+ */
+abstract public class TBrowsableWidget extends TScrollableWidget {
+       private int selectedRow;
+       private int selectedColumn;
+       private int yOffset;
+
+       /**
+        * The number of rows in this {@link TWidget}.
+        * 
+        * @return the number of rows
+        */
+       abstract protected int getRowCount();
+
+       /**
+        * The number of columns in this {@link TWidget}.
+        * 
+        * @return the number of columns
+        */
+       abstract protected int getColumnCount();
+
+       /**
+        * The virtual width of this {@link TWidget}, that is, the total width it
+        * can take to display all the data.
+        * 
+        * @return the width
+        */
+       abstract int getVirtualWidth();
+
+       /**
+        * The virtual height of this {@link TWidget}, that is, the total width it
+        * can take to display all the data.
+        * 
+        * @return the height
+        */
+       abstract int getVirtualHeight();
+
+       /**
+        * Basic setup of this class (called by all constructors)
+        */
+       private void setup() {
+               vScroller = new TVScroller(this, 0, 0, 1);
+               hScroller = new THScroller(this, 0, 0, 1);
+               fixScrollers();
+       }
+
+       /**
+        * Create a new {@link TBrowsableWidget} linked to the given {@link TWidget}
+        * parent.
+        * 
+        * @param parent
+        *            parent widget
+        */
+       protected TBrowsableWidget(final TWidget parent) {
+               super(parent);
+               setup();
+       }
+
+       /**
+        * Create a new {@link TBrowsableWidget} linked to the given {@link TWidget}
+        * parent.
+        * 
+        * @param parent
+        *            parent widget
+        * @param x
+        *            column relative to parent
+        * @param y
+        *            row relative to parent
+        * @param width
+        *            width of widget
+        * @param height
+        *            height of widget
+        */
+       protected TBrowsableWidget(final TWidget parent, final int x, final int y,
+                       final int width, final int height) {
+               super(parent, x, y, width, height);
+               setup();
+       }
+
+       /**
+        * Create a new {@link TBrowsableWidget} linked to the given {@link TWidget}
+        * parent.
+        * 
+        * @param parent
+        *            parent widget
+        * @param enabled
+        *            if true assume enabled
+        */
+       protected TBrowsableWidget(final TWidget parent, final boolean enabled) {
+               super(parent, enabled);
+               setup();
+       }
+
+       /**
+        * Create a new {@link TBrowsableWidget} linked to the given {@link TWidget}
+        * parent.
+        * 
+        * @param parent
+        *            parent widget
+        * @param enabled
+        *            if true assume enabled
+        * @param x
+        *            column relative to parent
+        * @param y
+        *            row relative to parent
+        * @param width
+        *            width of widget
+        * @param height
+        *            height of widget
+        */
+       protected TBrowsableWidget(final TWidget parent, final boolean enabled,
+                       final int x, final int y, final int width, final int height) {
+               super(parent, enabled, x, y, width, height);
+               setup();
+       }
+
+       /**
+        * The currently selected row (or -1 if no row is selected).
+        * 
+        * @return the selected row
+        */
+       public int getSelectedRow() {
+               return selectedRow;
+       }
+
+       /**
+        * The currently selected row (or -1 if no row is selected).
+        * <p>
+        * You may want to call {@link TBrowsableWidget#reflowData()} when done to
+        * see the changes.
+        * 
+        * @param selectedRow
+        *            the new selected row
+        * 
+        * @throws IndexOutOfBoundsException
+        *             when the index is out of bounds
+        */
+       public void setSelectedRow(int selectedRow) {
+               if (selectedRow < -1 || selectedRow >= getRowCount()) {
+                       throw new IndexOutOfBoundsException(String.format(
+                                       "Cannot set row %d on a table with %d rows", selectedRow,
+                                       getRowCount()));
+               }
+
+               this.selectedRow = selectedRow;
+       }
+
+       /**
+        * The currently selected column (or -1 if no column is selected).
+        * 
+        * @return the new selected column
+        */
+       public int getSelectedColumn() {
+               return selectedColumn;
+       }
+
+       /**
+        * The currently selected column (or -1 if no column is selected).
+        * <p>
+        * You may want to call {@link TBrowsableWidget#reflowData()} when done to
+        * see the changes.
+        * 
+        * @param selectedColumn
+        *            the new selected column
+        * 
+        * @throws IndexOutOfBoundsException
+        *             when the index is out of bounds
+        */
+       public void setSelectedColumn(int selectedColumn) {
+               if (selectedColumn < -1 || selectedColumn >= getColumnCount()) {
+                       throw new IndexOutOfBoundsException(String.format(
+                                       "Cannot set column %d on a table with %d columns",
+                                       selectedColumn, getColumnCount()));
+               }
+
+               this.selectedColumn = selectedColumn;
+       }
+
+       /**
+        * An offset on the Y position of the table, i.e., the number of rows to
+        * skip so the control can draw that many rows always on top.
+        * 
+        * @return the offset
+        */
+       public int getYOffset() {
+               return yOffset;
+       }
+
+       /**
+        * An offset on the Y position of the table, i.e., the number of rows that
+        * should always stay on top.
+        * 
+        * @param yOffset
+        *            the new offset
+        */
+       public void setYOffset(int yOffset) {
+               this.yOffset = yOffset;
+       }
+
+       @SuppressWarnings("unused")
+       public void dispatchMove(int fromRow, int toRow) {
+               reflowData();
+       }
+
+       @SuppressWarnings("unused")
+       public void dispatchEnter(int selectedRow) {
+               reflowData();
+       }
+
+       @Override
+       public void onMouseDown(final TMouseEvent mouse) {
+               if (mouse.isMouseWheelUp()) {
+                       vScroller.decrement();
+                       return;
+               }
+               if (mouse.isMouseWheelDown()) {
+                       vScroller.increment();
+                       return;
+               }
+
+               if ((mouse.getX() < getWidth() - 1) && (mouse.getY() < getHeight() - 1)) {
+                       if (vScroller.getValue() + mouse.getY() < getRowCount()) {
+                               selectedRow = vScroller.getValue() + mouse.getY()
+                                               - getYOffset();
+                       }
+                       dispatchEnter(selectedRow);
+                       return;
+               }
+
+               // Pass to children
+               super.onMouseDown(mouse);
+       }
+
+       @Override
+       public void onKeypress(final TKeypressEvent keypress) {
+               int maxX = getRowCount();
+               int prevSelectedRow = selectedRow;
+
+               int firstLineIndex = vScroller.getValue() - getYOffset() + 2;
+               int lastLineIndex = firstLineIndex - hScroller.getHeight()
+                               + getHeight() - 2 - 2;
+
+               if (keypress.equals(kbLeft)) {
+                       hScroller.decrement();
+               } else if (keypress.equals(kbRight)) {
+                       hScroller.increment();
+               } else if (keypress.equals(kbUp)) {
+                       if (maxX > 0 && selectedRow < maxX) {
+                               if (selectedRow > 0) {
+                                       if (selectedRow <= firstLineIndex) {
+                                               vScroller.decrement();
+                                       }
+                                       selectedRow--;
+                               } else {
+                                       selectedRow = 0;
+                               }
+
+                               dispatchMove(prevSelectedRow, selectedRow);
+                       }
+               } else if (keypress.equals(kbDown)) {
+                       if (maxX > 0) {
+                               if (selectedRow >= 0) {
+                                       if (selectedRow < maxX - 1) {
+                                               selectedRow++;
+                                               if (selectedRow >= lastLineIndex) {
+                                                       vScroller.increment();
+                                               }
+                                       }
+                               } else {
+                                       selectedRow = 0;
+                               }
+
+                               dispatchMove(prevSelectedRow, selectedRow);
+                       }
+               } else if (keypress.equals(kbPgUp)) {
+                       if (selectedRow >= 0) {
+                               vScroller.bigDecrement();
+                               selectedRow -= getHeight() - 1;
+                               if (selectedRow < 0) {
+                                       selectedRow = 0;
+                               }
+
+                               dispatchMove(prevSelectedRow, selectedRow);
+                       }
+               } else if (keypress.equals(kbPgDn)) {
+                       if (selectedRow >= 0) {
+                               vScroller.bigIncrement();
+                               selectedRow += getHeight() - 1;
+                               if (selectedRow > getRowCount() - 1) {
+                                       selectedRow = getRowCount() - 1;
+                               }
+
+                               dispatchMove(prevSelectedRow, selectedRow);
+                       }
+               } else if (keypress.equals(kbHome)) {
+                       if (getRowCount() > 0) {
+                               vScroller.toTop();
+                               selectedRow = 0;
+                               dispatchMove(prevSelectedRow, selectedRow);
+                       }
+               } else if (keypress.equals(kbEnd)) {
+                       if (getRowCount() > 0) {
+                               vScroller.toBottom();
+                               selectedRow = getRowCount() - 1;
+                               dispatchMove(prevSelectedRow, selectedRow);
+                       }
+               } else if (keypress.equals(kbTab)) {
+                       getParent().switchWidget(true);
+               } else if (keypress.equals(kbShiftTab) || keypress.equals(kbBackTab)) {
+                       getParent().switchWidget(false);
+               } else if (keypress.equals(kbEnter)) {
+                       if (selectedRow >= 0) {
+                               dispatchEnter(selectedRow);
+                       }
+               } else {
+                       // Pass other keys (tab etc.) on
+                       super.onKeypress(keypress);
+               }
+       }
+
+       @Override
+       public void onResize(TResizeEvent event) {
+               super.onResize(event);
+               reflowData();
+       }
+
+       @Override
+       public void reflowData() {
+               super.reflowData();
+               fixScrollers();
+       }
+
+       private void fixScrollers() {
+               int width = getWidth() - 1; // vertical prio
+               int height = getHeight();
+
+               // TODO: why did we do that before?
+               if (false) {
+                       width -= 2;
+                       height = -1;
+               }
+
+               int x = Math.max(0, width);
+               int y = Math.max(0, height - 1);
+
+               vScroller.setX(x);
+               vScroller.setHeight(height);
+               hScroller.setY(y);
+               hScroller.setWidth(width);
+
+               // TODO why did we use to add 2?
+               // + 2 (for the border of the window)
+
+               // virtual_size
+               // + the other scroll bar size
+               vScroller.setTopValue(0);
+               vScroller.setBottomValue(Math.max(0, getVirtualHeight() - getHeight()
+                               + hScroller.getHeight()));
+               hScroller.setLeftValue(0);
+               hScroller.setRightValue(Math.max(0, getVirtualWidth() - getWidth()
+                               + vScroller.getWidth()));
+       }
+}
diff --git a/src/be/nikiroo/jexer/TSizeConstraint.java b/src/be/nikiroo/jexer/TSizeConstraint.java
new file mode 100644 (file)
index 0000000..bfdbb3a
--- /dev/null
@@ -0,0 +1,92 @@
+package be.nikiroo.jexer;
+
+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
+       //              coordinate NULL = do not work on that side at all
+       static public 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 public 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();
+                       }
+               }
+       }
+}
diff --git a/src/be/nikiroo/jexer/TTable.java b/src/be/nikiroo/jexer/TTable.java
new file mode 100644 (file)
index 0000000..45e5df2
--- /dev/null
@@ -0,0 +1,516 @@
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 David "Niki" ROULET
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author David ROULET [niki@nikiroo.be]
+ * @version 1
+ */
+package be.nikiroo.jexer;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import javax.swing.table.TableModel;
+
+import be.nikiroo.jexer.TTableCellRenderer.CellRendererMode;
+import jexer.TAction;
+import jexer.TWidget;
+import jexer.bits.CellAttributes;
+
+/**
+ * A table widget to display and browse through tabular data.
+ * <p>
+ * Currently, you can only select a line (a row) at a time, but the data you
+ * present is still tabular. You also access the data in a tabular way (by
+ * <tt>(raw,column)</tt>).
+ * 
+ * @author niki
+ */
+public class TTable extends TBrowsableWidget {
+       // Default renderers use text mode
+       static private TTableCellRenderer defaultSeparatorRenderer = new TTableCellRendererText(
+                       CellRendererMode.SEPARATOR);
+       static private TTableCellRenderer defaultHeaderRenderer = new TTableCellRendererText(
+                       CellRendererMode.HEADER);
+       static private TTableCellRenderer defaultHeaderSeparatorRenderer = new TTableCellRendererText(
+                       CellRendererMode.HEADER_SEPARATOR);
+
+       private boolean showHeader;
+
+       private List<TTableColumn> columns = new ArrayList<TTableColumn>();
+       private TableModel model;
+
+       private int selectedColumn;
+
+       private TTableCellRenderer separatorRenderer;
+       private TTableCellRenderer headerRenderer;
+       private TTableCellRenderer headerSeparatorRenderer;
+
+       /**
+        * The action to perform when the user selects an item (clicks or enter).
+        */
+       private TAction enterAction = null;
+
+       /**
+        * The action to perform when the user navigates with keyboard.
+        */
+       private TAction moveAction = null;
+
+       /**
+        * Create a new {@link TTable}.
+        * 
+        * @param parent
+        *            the parent widget
+        * @param x
+        *            the X position
+        * @param y
+        *            the Y position
+        * @param width
+        *            the width of the {@link TTable}
+        * @param height
+        *            the height of the {@link TTable}
+        * @param enterAction
+        *            an action to call when a cell is selected
+        * @param moveAction
+        *            an action to call when the currently active cell is changed
+        */
+       public TTable(TWidget parent, int x, int y, int width, int height,
+                       final TAction enterAction, final TAction moveAction) {
+               this(parent, x, y, width, height, enterAction, moveAction, null, false);
+       }
+
+       /**
+        * Create a new {@link TTable}.
+        * 
+        * @param parent
+        *            the parent widget
+        * @param x
+        *            the X position
+        * @param y
+        *            the Y position
+        * @param width
+        *            the width of the {@link TTable}
+        * @param height
+        *            the height of the {@link TTable}
+        * @param enterAction
+        *            an action to call when a cell is selected
+        * @param moveAction
+        *            an action to call when the currently active cell is changed
+        * @param headers
+        *            the headers of the {@link TTable}
+        * @param showHeaders
+        *            TRUE to show the headers on screen
+        */
+       public TTable(TWidget parent, int x, int y, int width, int height,
+                       final TAction enterAction, final TAction moveAction,
+                       List<? extends Object> headers, boolean showHeaders) {
+               super(parent, x, y, width, height);
+
+               this.model = new TTableModel(new Object[][] {});
+               setSelectedRow(-1);
+               this.selectedColumn = -1;
+
+               setHeaders(headers, showHeaders);
+
+               this.enterAction = enterAction;
+               this.moveAction = moveAction;
+
+               reflowData();
+       }
+
+       /**
+        * The data model (containing the actual data) used by this {@link TTable},
+        * as with the usual Swing tables.
+        * 
+        * @return the model
+        */
+       public TableModel getModel() {
+               return model;
+       }
+
+       /**
+        * The data model (containing the actual data) used by this {@link TTable},
+        * as with the usual Swing tables.
+        * <p>
+        * Will reset all the rendering cells.
+        * 
+        * @param model
+        *            the new model
+        */
+       public void setModel(TableModel model) {
+               this.model = model;
+               reflowData();
+       }
+
+       /**
+        * The columns used by this {@link TTable} (you need to access them if you
+        * want to change the way they are rendered, for instance, or their size).
+        * 
+        * @return the columns
+        */
+       public List<TTableColumn> getColumns() {
+               return columns;
+       }
+
+       /**
+        * The {@link TTableCellRenderer} used by the separators (one separator
+        * between two data columns).
+        * 
+        * @return the renderer, or the default one if none is set (never NULL)
+        */
+       public TTableCellRenderer getSeparatorRenderer() {
+               return separatorRenderer != null ? separatorRenderer
+                               : defaultSeparatorRenderer;
+       }
+
+       /**
+        * The {@link TTableCellRenderer} used by the separators (one separator
+        * between two data columns).
+        * 
+        * @param separatorRenderer
+        *            the new renderer, or NULL to use the default renderer
+        */
+       public void setSeparatorRenderer(TTableCellRenderer separatorRenderer) {
+               this.separatorRenderer = separatorRenderer;
+       }
+
+       /**
+        * The {@link TTableCellRenderer} used by the headers (if
+        * {@link TTable#isShowHeader()} is enabled, the first line represents the
+        * headers with the column names).
+        * 
+        * @return the renderer, or the default one if none is set (never NULL)
+        */
+       public TTableCellRenderer getHeaderRenderer() {
+               return headerRenderer != null ? headerRenderer : defaultHeaderRenderer;
+       }
+
+       /**
+        * The {@link TTableCellRenderer} used by the headers (if
+        * {@link TTable#isShowHeader()} is enabled, the first line represents the
+        * headers with the column names).
+        * 
+        * @param headerRenderer
+        *            the new renderer, or NULL to use the default renderer
+        */
+       public void setHeaderRenderer(TTableCellRenderer headerRenderer) {
+               this.headerRenderer = headerRenderer;
+       }
+
+       /**
+        * The {@link TTableCellRenderer} to use on separators in header lines (see
+        * the related methods to understand what each of them is).
+        * 
+        * @return the renderer, or the default one if none is set (never NULL)
+        */
+       public TTableCellRenderer getHeaderSeparatorRenderer() {
+               return headerSeparatorRenderer != null ? headerSeparatorRenderer
+                               : defaultHeaderSeparatorRenderer;
+       }
+
+       /**
+        * The {@link TTableCellRenderer} to use on separators in header lines (see
+        * the related methods to understand what each of them is).
+        * 
+        * @param headerSeparatorRenderer
+        *            the new renderer, or NULL to use the default renderer
+        */
+       public void setHeaderSeparatorRenderer(
+                       TTableCellRenderer headerSeparatorRenderer) {
+               this.headerSeparatorRenderer = headerSeparatorRenderer;
+       }
+
+       /**
+        * Show the header row on this {@link TTable}.
+        * 
+        * @return TRUE if we show them
+        */
+       public boolean isShowHeader() {
+               return showHeader;
+       }
+
+       /**
+        * Show the header row on this {@link TTable}.
+        * 
+        * @param showHeader
+        *            TRUE to show them
+        */
+       public void setShowHeader(boolean showHeader) {
+               this.showHeader = showHeader;
+               setYOffset(showHeader ? 2 : 0);
+               reflowData();
+       }
+
+       /**
+        * Change the headers of the table.
+        * <p>
+        * Note that this method is a convenience method that will create columns of
+        * the corresponding names and set them. As such, the previous columns if
+        * any will be replaced.
+        * 
+        * @param headers
+        *            the new headers
+        */
+       public void setHeaders(List<? extends Object> headers) {
+               setHeaders(headers, showHeader);
+       }
+
+       /**
+        * Change the headers of the table.
+        * <p>
+        * Note that this method is a convenience method that will create columns of
+        * the corresponding names and set them in the same order. As such, the
+        * previous columns if any will be replaced.
+        * 
+        * @param headers
+        *            the new headers
+        * @param showHeader
+        *            TRUE to show them on screen
+        */
+       public void setHeaders(List<? extends Object> headers, boolean showHeader) {
+               if (headers == null) {
+                       headers = new ArrayList<Object>();
+               }
+
+               int i = 0;
+               this.columns = new ArrayList<TTableColumn>();
+               for (Object header : headers) {
+                       this.columns.add(new TTableColumn(i++, header, getModel()));
+               }
+
+               setShowHeader(showHeader);
+       }
+
+       /**
+        * Set the data and create a new {@link TTableModel} for them.
+        * 
+        * @param data
+        *            the data to set into this table, as an array of rows, that is,
+        *            an array of arrays of values
+        */
+
+       public void setRowData(Object[][] data) {
+               setRowData(TTableModel.convert(data));
+       }
+
+       /**
+        * Set the data and create a new {@link TTableModel} for them.
+        * 
+        * @param data
+        *            the data to set into this table, as a collection of rows, that
+        *            is, a collection of collections of values
+        */
+       public void setRowData(
+                       final Collection<? extends Collection<? extends Object>> data) {
+               setModel(new TTableModel(data));
+       }
+
+       /**
+        * The currently selected cell.
+        * 
+        * @return the cell
+        */
+       public Object getSelectedCell() {
+               int selectedRow = getSelectedRow();
+               if (selectedRow >= 0 && selectedColumn >= 0) {
+                       return model.getValueAt(selectedRow, selectedColumn);
+               }
+
+               return null;
+       }
+
+       @Override
+       public int getRowCount() {
+               if (model == null) {
+                       return 0;
+               }
+               return model.getRowCount();
+       }
+
+       @Override
+       public int getColumnCount() {
+               if (model == null) {
+                       return 0;
+               }
+               return model.getColumnCount();
+       }
+
+       @Override
+       public void dispatchEnter(int selectedRow) {
+               super.dispatchEnter(selectedRow);
+               if (enterAction != null) {
+                       enterAction.DO();
+               }
+       }
+
+       @Override
+       public void dispatchMove(int fromRow, int toRow) {
+               super.dispatchMove(fromRow, toRow);
+               if (moveAction != null) {
+                       moveAction.DO();
+               }
+       }
+
+       /**
+        * Clear the content of the {@link TTable}.
+        * <p>
+        * It will not affect the headers.
+        * <p>
+        * You may want to call {@link TTable#reflowData()} when done to see the
+        * changes.
+        */
+       public void clear() {
+               setSelectedRow(-1);
+               selectedColumn = -1;
+               setModel(new TTableModel(new Object[][] {}));
+       }
+
+       @Override
+       public void reflowData() {
+               super.reflowData();
+
+               int lastAutoColumn = -1;
+               int rowWidth = 0;
+
+               int i = 0;
+               for (TTableColumn tcol : columns) {
+                       tcol.reflowData();
+
+                       if (!tcol.isForcedWidth()) {
+                               lastAutoColumn = i;
+                       }
+
+                       rowWidth += tcol.getWidth();
+
+                       i++;
+               }
+
+               if (!columns.isEmpty()) {
+                       rowWidth += (i - 1) * getSeparatorRenderer().getWidthOf(null);
+
+                       int extraWidth = getWidth() - rowWidth;
+                       if (extraWidth > 0) {
+                               if (lastAutoColumn < 0) {
+                                       lastAutoColumn = columns.size() - 1;
+                               }
+                               TTableColumn tcol = columns.get(lastAutoColumn);
+                               tcol.expandWidthTo(tcol.getWidth() + extraWidth);
+                               rowWidth += extraWidth;
+                       }
+               }
+       }
+
+       @Override
+       public void draw() {
+               int begin = vScroller.getValue();
+               int y = this.showHeader ? 2 : 0;
+
+               if (showHeader) {
+                       CellAttributes colorHeaders = getHeaderRenderer()
+                                       .getCellAttributes(getTheme(), false, isAbsoluteActive());
+                       drawRow(-1, 0);
+                       String formatString = "%-" + Integer.toString(getWidth()) + "s";
+                       String data = String.format(formatString, "");
+                       getScreen().putStringXY(0, 1, data, colorHeaders);
+               }
+
+               // draw the actual rows until no more,
+               // then pad the rest with blank rows
+               for (int i = begin; i < getRowCount(); i++) {
+                       drawRow(i, y);
+                       y++;
+
+                       // -2: window borders
+                       if (y >= getHeight() - 2 - getHorizontalScroller().getHeight()) {
+                               break;
+                       }
+               }
+
+               CellAttributes emptyRowColor = getSeparatorRenderer()
+                               .getCellAttributes(getTheme(), false, isAbsoluteActive());
+               for (int i = getRowCount(); i < getHeight(); i++) {
+                       getScreen().hLineXY(0, y, getWidth() - 1, ' ', emptyRowColor);
+                       y++;
+               }
+       }
+
+       @Override
+       protected int getVirtualWidth() {
+               int width = 0;
+
+               if (getColumns() != null) {
+                       for (TTableColumn tcol : getColumns()) {
+                               width += tcol.getWidth();
+                       }
+
+                       if (getColumnCount() > 0) {
+                               width += (getColumnCount() - 1)
+                                               * getSeparatorRenderer().getWidthOf(null);
+                       }
+               }
+
+               return width;
+       }
+
+       @Override
+       protected int getVirtualHeight() {
+               // TODO: allow changing the height of one row
+               return (showHeader ? 2 : 0) + (getRowCount() * 1);
+       }
+
+       /**
+        * Draw the given row (it <b>MUST</b> exist) at the specified index and
+        * offset.
+        * 
+        * @param rowIndex
+        *            the index of the row to draw or -1 for the headers
+        * @param y
+        *            the Y position
+        */
+       private void drawRow(int rowIndex, int y) {
+               for (int i = 0; i < getColumnCount(); i++) {
+                       TTableColumn tcol = columns.get(i);
+                       Object value;
+                       if (rowIndex < 0) {
+                               value = tcol.getHeaderValue();
+                       } else {
+                               value = model.getValueAt(rowIndex, tcol.getModelIndex());
+                       }
+
+                       if (i > 0) {
+                               TTableCellRenderer sep = rowIndex < 0 ? getHeaderSeparatorRenderer()
+                                               : getSeparatorRenderer();
+                               sep.renderTableCell(this, null, rowIndex, i - 1, y);
+                       }
+
+                       if (rowIndex < 0) {
+                               getHeaderRenderer()
+                                               .renderTableCell(this, value, rowIndex, i, y);
+                       } else {
+                               tcol.getRenderer().renderTableCell(this, value, rowIndex, i, y);
+                       }
+               }
+       }
+}
diff --git a/src/be/nikiroo/jexer/TTableCellRenderer.java b/src/be/nikiroo/jexer/TTableCellRenderer.java
new file mode 100644 (file)
index 0000000..6d7b3b3
--- /dev/null
@@ -0,0 +1,240 @@
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 David "Niki" ROULET
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author David ROULET [niki@nikiroo.be]
+ * @version 1
+ */
+package be.nikiroo.jexer;
+
+import jexer.bits.CellAttributes;
+import jexer.bits.ColorTheme;
+
+/**
+ * A {@link TTable} cell renderer allows you to customize the way a single cell
+ * will be displayed on screen.
+ * <p>
+ * It can be used in a {@link TTable} for the haeders or the separators or in a
+ * {@link TTableColumn} for the data.
+ * 
+ * @author niki
+ */
+abstract public class TTableCellRenderer {
+       private CellRendererMode mode;
+
+       /**
+        * The simple renderer mode.
+        * 
+        * @author niki
+        */
+       public enum CellRendererMode {
+               /** Normal text mode */
+               NORMAL,
+               /** Only display a separator */
+               SEPARATOR,
+               /** Header text mode */
+               HEADER,
+               /** Both HEADER and SEPARATOR at once */
+               HEADER_SEPARATOR;
+
+               /**
+                * This mode represents a separator.
+                * 
+                * @return TRUE for separators
+                */
+               public boolean isSeparator() {
+                       return this == SEPARATOR || this == HEADER_SEPARATOR;
+               }
+
+               /**
+                * This mode represents a header.
+                * 
+                * @return TRUE for headers
+                */
+               public boolean isHeader() {
+                       return this == HEADER || this == HEADER_SEPARATOR;
+               }
+       }
+
+       /**
+        * Create a new renderer of the given mode.
+        * 
+        * @param mode
+        *            the renderer mode, cannot be NULL
+        */
+       public TTableCellRenderer(CellRendererMode mode) {
+               if (mode == null) {
+                       throw new IllegalArgumentException(
+                                       "Cannot create a renderer of type NULL");
+               }
+
+               this.mode = mode;
+       }
+
+       /**
+        * Render the given value.
+        * 
+        * @param table
+        *            the table to write on
+        * @param value
+        *            the value to write
+        * @param rowIndex
+        *            the row index in the table
+        * @param colIndex
+        *            the column index in the table
+        * @param y
+        *            the Y position at which to draw this row
+        */
+       abstract public void renderTableCell(TTable table, Object value,
+                       int rowIndex, int colIndex, int y);
+
+       /**
+        * The mode of this {@link TTableCellRenderer}.
+        * 
+        * @return the mode
+        */
+       public CellRendererMode getMode() {
+               return mode;
+       }
+
+       /**
+        * The cell attributes to use for the given state.
+        * 
+        * @param theme
+        *            the color theme to use
+        * @param isSelected
+        *            TRUE if the cell is selected
+        * @param hasFocus
+        *            TRUE if the cell has focus
+        * 
+        * @return the attributes
+        */
+       public CellAttributes getCellAttributes(ColorTheme theme,
+                       boolean isSelected, boolean hasFocus) {
+               return theme.getColor(getColorKey(isSelected, hasFocus));
+       }
+
+       /**
+        * Measure the width of the value.
+        * 
+        * @param value
+        *            the value to measure
+        * 
+        * @return its width
+        */
+       public int getWidthOf(Object value) {
+               if (getMode().isSeparator()) {
+                       return asText(null, 0, false).length();
+               }
+               return ("" + value).length();
+       }
+
+       /**
+        * The colour to use for the given state, specified as a Jexer colour key.
+        * 
+        * @param isSelected
+        *            TRUE if the cell is selected
+        * @param hasFocus
+        *            TRUE if the cell has focus
+        * 
+        * @return the colour key
+        */
+       protected String getColorKey(boolean isSelected, boolean hasFocus) {
+               if (mode.isHeader()) {
+                       return "tlabel";
+               }
+
+               String colorKey = "tlist";
+               if (isSelected) {
+                       colorKey += ".selected";
+               } else if (!hasFocus) {
+                       colorKey += ".inactive";
+               }
+
+               return colorKey;
+       }
+
+       /**
+        * Return the X offset to use to draw a column at the given index.
+        * 
+        * @param table
+        *            the table to draw into
+        * @param colIndex
+        *            the column index
+        * 
+        * @return the offset
+        */
+       protected int getXOffset(TTable table, int colIndex) {
+               int xOffset = -table.getHorizontalValue();
+               for (int i = 0; i <= colIndex; i++) {
+                       TTableColumn tcol = table.getColumns().get(i);
+                       xOffset += tcol.getWidth();
+                       if (i > 0) {
+                               xOffset += table.getSeparatorRenderer().getWidthOf(null);
+                       }
+               }
+
+               TTableColumn tcol = table.getColumns().get(colIndex);
+               if (!getMode().isSeparator()) {
+                       xOffset -= tcol.getWidth();
+               }
+
+               return xOffset;
+       }
+
+       /**
+        * Return the text to use (usually the converted-to-text value, except for
+        * the special separator mode).
+        * 
+        * @param value
+        *            the value to get the text of
+        * @param width
+        *            the width we should tale
+        * @param align
+        *            the text to the right
+        * 
+        * @return the {@link String} to display
+        */
+       protected String asText(Object value, int width, boolean rightAlign) {
+               if (getMode().isSeparator()) {
+                       // some nice characters for the separator: â”ƒ â”‚ |
+                       return " â”‚ ";
+               }
+
+               if (width <= 0) {
+                       return "";
+               }
+
+               String format;
+               if (!rightAlign) {
+                       // Left align
+                       format = "%-" + width + "s";
+               } else {
+                       // right align
+                       format = "%" + width + "s";
+               }
+
+               return String.format(format, value);
+       }
+}
\ No newline at end of file
diff --git a/src/be/nikiroo/jexer/TTableCellRendererText.java b/src/be/nikiroo/jexer/TTableCellRendererText.java
new file mode 100644 (file)
index 0000000..8f81883
--- /dev/null
@@ -0,0 +1,91 @@
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 David "Niki" ROULET
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author David ROULET [niki@nikiroo.be]
+ * @version 1
+ */
+package be.nikiroo.jexer;
+
+import jexer.bits.CellAttributes;
+
+/**
+ * A simple {@link TTableCellRenderer} that display the values within a
+ * {@link TLabel}.
+ * <p>
+ * It supports a few different modes, see
+ * {@link TTableOldSimpleTextCellRenderer.CellRendererMode}.
+ * 
+ * @author niki
+ */
+public class TTableCellRendererText extends TTableCellRenderer {
+       private boolean rightAlign;
+
+       /**
+        * Create a new renderer for normal text mode.
+        */
+       public TTableCellRendererText() {
+               this(CellRendererMode.NORMAL);
+       }
+
+       /**
+        * Create a new renderer of the given mode.
+        * 
+        * @param mode
+        *            the renderer mode
+        */
+       public TTableCellRendererText(CellRendererMode mode) {
+               this(mode, false);
+       }
+
+       /**
+        * Create a new renderer of the given mode.
+        * 
+        * @param mode
+        *            the renderer mode, cannot be NULL
+        */
+       public TTableCellRendererText(CellRendererMode mode,
+                       boolean rightAlign) {
+               super(mode);
+
+               this.rightAlign = rightAlign;
+       }
+
+       @Override
+       public void renderTableCell(TTable table, Object value, int rowIndex,
+                       int colIndex, int y) {
+
+               int xOffset = getXOffset(table, colIndex);
+               TTableColumn tcol = table.getColumns().get(colIndex);
+               String data = asText(value, tcol.getWidth(), rightAlign);
+
+               if (!data.isEmpty()) {
+                       boolean isSelected = table.getSelectedRow() == rowIndex;
+                       boolean hasFocus = table.isAbsoluteActive();
+                       CellAttributes color = getCellAttributes(table.getWindow()
+                                       .getApplication().getTheme(), isSelected, hasFocus);
+                       table.getScreen().putStringXY(xOffset, y, data, color);
+               }
+       }
+}
diff --git a/src/be/nikiroo/jexer/TTableCellRendererWidget.java b/src/be/nikiroo/jexer/TTableCellRendererWidget.java
new file mode 100644 (file)
index 0000000..22c6f47
--- /dev/null
@@ -0,0 +1,170 @@
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 David "Niki" ROULET
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author David ROULET [niki@nikiroo.be]
+ * @version 1
+ */
+package be.nikiroo.jexer;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import jexer.TLabel;
+import jexer.TWidget;
+
+/**
+ * A simple {@link TTableCellRenderer} that display the values within a
+ * {@link TLabel}.
+ * <p>
+ * It supports a few different modes, see
+ * {@link TTableSimpleTextCellRenderer.CellRendererMode}.
+ * 
+ * @author niki
+ */
+public class TTableCellRendererWidget extends TTableCellRenderer {
+       private boolean rightAlign;
+       private Map<String, TWidget> widgets = new HashMap<String, TWidget>();
+
+       /**
+        * Create a new renderer for normal text mode.
+        */
+       public TTableCellRendererWidget() {
+               this(CellRendererMode.NORMAL);
+       }
+
+       /**
+        * Create a new renderer of the given mode.
+        * 
+        * @param mode
+        *            the renderer mode
+        */
+       public TTableCellRendererWidget(CellRendererMode mode) {
+               this(mode, false);
+       }
+
+       /**
+        * Create a new renderer of the given mode.
+        * 
+        * @param mode
+        *            the renderer mode, cannot be NULL
+        */
+       public TTableCellRendererWidget(CellRendererMode mode, boolean rightAlign) {
+               super(mode);
+
+               this.rightAlign = rightAlign;
+       }
+
+       @Override
+       public void renderTableCell(TTable table, Object value, int rowIndex,
+                       int colIndex, int y) {
+
+               String wkey = "[Row " + y + " " + getMode() + "]";
+               TWidget widget = widgets.get(wkey);
+
+               TTableColumn tcol = table.getColumns().get(colIndex);
+               boolean isSelected = table.getSelectedRow() == rowIndex;
+               boolean hasFocus = table.isAbsoluteActive();
+               int width = tcol.getWidth();
+
+               int xOffset = getXOffset(table, colIndex);
+
+               if (widget != null
+                               && !updateTableCellRendererComponent(widget, value, isSelected,
+                                               hasFocus, y, xOffset, width)) {
+                       table.removeChild(widget);
+                       widget = null;
+               }
+
+               if (widget == null) {
+                       widget = getTableCellRendererComponent(table, value, isSelected,
+                                       hasFocus, y, xOffset, width);
+               }
+
+               widgets.put(wkey, widget);
+       }
+
+       /**
+        * Create a new {@link TWidget} to represent the given value.
+        * 
+        * @param table
+        *            the parent {@link TTable}
+        * @param value
+        *            the value to represent
+        * @param isSelected
+        *            TRUE if selected
+        * @param hasFocus
+        *            TRUE if focused
+        * @param row
+        *            the row to draw it at
+        * @param column
+        *            the column to draw it at
+        * @param width
+        *            the width of the control
+        * 
+        * @return the widget
+        */
+       protected TWidget getTableCellRendererComponent(TTable table, Object value,
+                       boolean isSelected, boolean hasFocus, int row, int column, int width) {
+               return new TLabel(table, asText(value, width, rightAlign), column, row,
+                               getColorKey(isSelected, hasFocus), false);
+       }
+
+       /**
+        * Update the content of the widget if at all possible.
+        * 
+        * @param component
+        *            the component to update
+        * @param value
+        *            the value to represent
+        * @param isSelected
+        *            TRUE if selected
+        * @param hasFocus
+        *            TRUE if focused
+        * @param row
+        *            the row to draw it at
+        * @param column
+        *            the column to draw it at
+        * @param width
+        *            the width of the control
+        * 
+        * @return TRUE if the operation was possible, FALSE if it failed
+        */
+       protected boolean updateTableCellRendererComponent(TWidget component,
+                       Object value, boolean isSelected, boolean hasFocus, int row,
+                       int column, int width) {
+
+               if (component instanceof TLabel) {
+                       TLabel widget = (TLabel) component;
+                       widget.setLabel(asText(value, width, rightAlign));
+                       widget.setColorKey(getColorKey(isSelected, hasFocus));
+                       widget.setWidth(width);
+                       widget.setX(column);
+                       widget.setY(row);
+                       return true;
+               }
+
+               return false;
+       }
+}
diff --git a/src/be/nikiroo/jexer/TTableColumn.java b/src/be/nikiroo/jexer/TTableColumn.java
new file mode 100644 (file)
index 0000000..3eea230
--- /dev/null
@@ -0,0 +1,129 @@
+package be.nikiroo.jexer;
+
+import javax.swing.table.TableModel;
+
+import be.nikiroo.jexer.TTableCellRenderer.CellRendererMode;
+
+public class TTableColumn {
+       static private TTableCellRenderer defaultrenderer = new TTableCellRendererText(
+                       CellRendererMode.NORMAL);
+
+       private TableModel model;
+       private int modelIndex;
+       private int width;
+       private boolean forcedWidth;
+
+       private TTableCellRenderer renderer;
+
+       /** The auto-computed width of the column (the width of the largest value) */
+       private int autoWidth;
+
+       private Object headerValue;
+
+       public TTableColumn(int modelIndex) {
+               this(modelIndex, null);
+       }
+
+       public TTableColumn(int modelIndex, String colName) {
+               this(modelIndex, colName, null);
+       }
+
+       // set the width and preferred with the the max data size
+       public TTableColumn(int modelIndex, Object colValue, TableModel model) {
+               this.model = model;
+               this.modelIndex = modelIndex;
+
+               reflowData();
+
+               if (colValue != null) {
+                       setHeaderValue(colValue);
+               }
+       }
+
+       // never null
+       public TTableCellRenderer getRenderer() {
+               return renderer != null ? renderer : defaultrenderer;
+       }
+
+       public void setCellRenderer(TTableCellRenderer renderer) {
+               this.renderer = renderer;
+       }
+
+       /**
+        * Recompute whatever data is displayed by this widget.
+        * <p>
+        * Will just update the sizes in this case.
+        */
+       public void reflowData() {
+               if (model != null) {
+                       int maxDataSize = 0;
+                       for (int i = 0; i < model.getRowCount(); i++) {
+                               maxDataSize = Math.max(
+                                               maxDataSize,
+                                               getRenderer().getWidthOf(
+                                                               model.getValueAt(i, modelIndex)));
+                       }
+
+                       autoWidth = maxDataSize;
+                       if (!forcedWidth) {
+                               setWidth(maxDataSize);
+                       }
+               } else {
+                       autoWidth = 0;
+                       forcedWidth = false;
+                       width = 0;
+               }
+       }
+
+       public int getModelIndex() {
+               return modelIndex;
+       }
+
+       /**
+        * The actual size of the column. This can be auto-computed in some cases.
+        * 
+        * @return the width (never &lt; 0)
+        */
+       public int getWidth() {
+               return width;
+       }
+
+       /**
+        * Set the actual size of the column or -1 for auto size.
+        * 
+        * @param width
+        *            the width (or -1 for auto)
+        */
+       public void setWidth(int width) {
+               forcedWidth = width >= 0;
+
+               if (forcedWidth) {
+                       this.width = width;
+               } else {
+                       this.width = autoWidth;
+               }
+       }
+
+       /**
+        * The width was forced by the user (using
+        * {@link TTableColumn#setWidth(int)} with a positive value).
+        * 
+        * @return TRUE if it was
+        */
+       public boolean isForcedWidth() {
+               return forcedWidth;
+       }
+
+       // not an actual forced width, but does change the width return
+       void expandWidthTo(int width) {
+               this.width = width;
+       }
+
+       public Object getHeaderValue() {
+               return headerValue;
+       }
+
+       public void setHeaderValue(Object headerValue) {
+               this.headerValue = headerValue;
+       }
+}
diff --git a/src/be/nikiroo/jexer/TTableLine.java b/src/be/nikiroo/jexer/TTableLine.java
new file mode 100644 (file)
index 0000000..f393621
--- /dev/null
@@ -0,0 +1,135 @@
+package be.nikiroo.jexer;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+
+public class TTableLine implements List<String> {
+       //TODO: in TTable: default to header of size 1
+       private List<String> list;
+
+       public TTableLine(List<String> list) {
+               this.list = list;
+       }
+
+       // TODO: override this and the rest shall follow
+       protected List<String> getList() {
+               return list;
+       }
+
+       @Override
+       public int size() {
+               return getList().size();
+       }
+
+       @Override
+       public boolean isEmpty() {
+               return getList().isEmpty();
+       }
+
+       @Override
+       public boolean contains(Object o) {
+               return getList().contains(o);
+       }
+
+       @Override
+       public Iterator<String> iterator() {
+               return getList().iterator();
+       }
+
+       @Override
+       public Object[] toArray() {
+               return getList().toArray();
+       }
+
+       @Override
+       public <T> T[] toArray(T[] a) {
+               return getList().toArray(a);
+       }
+
+       @Override
+       public boolean containsAll(Collection<?> c) {
+               return getList().containsAll(c);
+       }
+
+       @Override
+       public String get(int index) {
+               return getList().get(index);
+       }
+
+       @Override
+       public int indexOf(Object o) {
+               return getList().indexOf(o);
+       }
+
+       @Override
+       public int lastIndexOf(Object o) {
+               return getList().lastIndexOf(o);
+       }
+
+       @Override
+       public List<String> subList(int fromIndex, int toIndex) {
+               return getList().subList(fromIndex, toIndex);
+       }
+
+       @Override
+       public ListIterator<String> listIterator() {
+               return getList().listIterator();
+       }
+
+       @Override
+       public ListIterator<String> listIterator(int index) {
+               return getList().listIterator(index);
+       }
+
+       @Override
+       public boolean add(String e) {
+               throw new UnsupportedOperationException("Read-only collection");
+       }
+
+       @Override
+       public boolean remove(Object o) {
+               throw new UnsupportedOperationException("Read-only collection");
+       }
+
+       @Override
+       public boolean addAll(Collection<? extends String> c) {
+               throw new UnsupportedOperationException("Read-only collection");
+       }
+
+       @Override
+       public boolean addAll(int index, Collection<? extends String> c) {
+               throw new UnsupportedOperationException("Read-only collection");
+       }
+
+       @Override
+       public boolean removeAll(Collection<?> c) {
+               throw new UnsupportedOperationException("Read-only collection");
+       }
+
+       @Override
+       public boolean retainAll(Collection<?> c) {
+               throw new UnsupportedOperationException("Read-only collection");
+       }
+
+       @Override
+       public void clear() {
+               throw new UnsupportedOperationException("Read-only collection");
+       }
+
+       @Override
+       public String set(int index, String element) {
+               throw new UnsupportedOperationException("Read-only collection");
+       }
+
+       @Override
+       public void add(int index, String element) {
+               throw new UnsupportedOperationException("Read-only collection");
+       }
+
+       @Override
+       public String remove(int index) {
+               throw new UnsupportedOperationException("Read-only collection");
+       }
+}
diff --git a/src/be/nikiroo/jexer/TTableModel.java b/src/be/nikiroo/jexer/TTableModel.java
new file mode 100644 (file)
index 0000000..cd86d35
--- /dev/null
@@ -0,0 +1,176 @@
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 David "Niki" ROULET
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author David ROULET [niki@nikiroo.be]
+ * @version 1
+ */
+package be.nikiroo.jexer;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+
+import javax.swing.event.TableModelListener;
+import javax.swing.table.AbstractTableModel;
+import javax.swing.table.TableModel;
+
+/**
+ * The model of a {@link TTable}. It contains the data of the table and allows
+ * you access to it.
+ * <p>
+ * Note that you don't need to send it the representation of the data, but the
+ * data itself; {@link TTableCellRenderer} is the class responsible of
+ * representing that data (you can change the headers renderer on a
+ * {@link TTable} and the cells renderer on each of its {@link TTableColumn}).
+ * <p>
+ * It works in a similar way to the Java Swing version of it.
+ * 
+ * @author niki
+ */
+public class TTableModel implements TableModel {
+       private TableModel model;
+
+       /**
+        * Create a new {@link TTableModel} with the given data inside.
+        * 
+        * @param data
+        *            the data
+        */
+       public TTableModel(Object[][] data) {
+               this(convert(data));
+       }
+
+       /**
+        * Create a new {@link TTableModel} with the given data inside.
+        * 
+        * @param data
+        *            the data
+        */
+       public TTableModel(
+                       final Collection<? extends Collection<? extends Object>> data) {
+
+               int maxItemsPerRow = 0;
+               for (Collection<? extends Object> rowOfData : data) {
+                       maxItemsPerRow = Math.max(maxItemsPerRow, rowOfData.size());
+               }
+
+               int i = 0;
+               final Object[][] odata = new Object[data.size()][maxItemsPerRow];
+               for (Collection<? extends Object> rowOfData : data) {
+                       odata[i] = new String[maxItemsPerRow];
+                       int j = 0;
+                       for (Object pieceOfData : rowOfData) {
+                               odata[i][j] = pieceOfData;
+                               j++;
+                       }
+                       i++;
+               }
+
+               final int maxItemsPerRowFinal = maxItemsPerRow;
+               this.model = new AbstractTableModel() {
+                       private static final long serialVersionUID = 1L;
+
+                       @Override
+                       public Object getValueAt(int rowIndex, int columnIndex) {
+                               return odata[rowIndex][columnIndex];
+                       }
+
+                       @Override
+                       public int getRowCount() {
+                               return odata.length;
+                       }
+
+                       @Override
+                       public int getColumnCount() {
+                               return maxItemsPerRowFinal;
+                       }
+               };
+       }
+
+       @Override
+       public int getRowCount() {
+               return model.getRowCount();
+       }
+
+       @Override
+       public int getColumnCount() {
+               return model.getColumnCount();
+       }
+
+       @Override
+       public String getColumnName(int columnIndex) {
+               return model.getColumnName(columnIndex);
+       }
+
+       @Override
+       public Class<?> getColumnClass(int columnIndex) {
+               return model.getColumnClass(columnIndex);
+       }
+
+       @Override
+       public boolean isCellEditable(int rowIndex, int columnIndex) {
+               return model.isCellEditable(rowIndex, columnIndex);
+       }
+
+       @Override
+       public Object getValueAt(int rowIndex, int columnIndex) {
+               return model.getValueAt(rowIndex, columnIndex);
+       }
+
+       @Override
+       public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
+               model.setValueAt(aValue, rowIndex, columnIndex);
+       }
+
+       @Override
+       public void addTableModelListener(TableModelListener l) {
+               model.addTableModelListener(l);
+       }
+
+       @Override
+       public void removeTableModelListener(TableModelListener l) {
+               model.removeTableModelListener(l);
+       }
+
+       /**
+        * Helper method to convert an array to a collection.
+        * 
+        * @param <T>
+        * 
+        * @param data
+        *            the data
+        * 
+        * @return the data in another format
+        */
+       static <T> Collection<Collection<T>> convert(T[][] data) {
+               Collection<Collection<T>> dataCollection = new ArrayList<Collection<T>>(
+                               data.length);
+               for (T pieceOfData[] : data) {
+                       dataCollection.add(Arrays.asList(pieceOfData));
+               }
+
+               return dataCollection;
+       }
+}