From: Niki Roo <niki@nikiroo.be>
Date: Mon, 10 Jul 2017 19:34:36 +0000 (+0200)
Subject: Prepare a new TUI version with Jexer (needs TTable)
X-Git-Url: https://git.nikiroo.be/?a=commitdiff_plain;h=10dd1e387d6a1834596ae70f48cf905d7b302131;p=jvcard.git

Prepare a new TUI version with Jexer (needs TTable)
---

diff --git a/src/be/nikiroo/jvcard/launcher/Optional.java b/src/be/nikiroo/jvcard/launcher/Optional.java
index 9ed726d..40592f3 100644
--- a/src/be/nikiroo/jvcard/launcher/Optional.java
+++ b/src/be/nikiroo/jvcard/launcher/Optional.java
@@ -1,6 +1,7 @@
 package be.nikiroo.jvcard.launcher;
 
 import java.io.IOException;
+import java.lang.reflect.Constructor;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.util.List;
@@ -21,7 +22,7 @@ import be.nikiroo.jvcard.launcher.CardResult.MergeCallback;
  * </p>
  * 
  * @author niki
- *
+ * 
  */
 class Optional {
 	/***
@@ -29,7 +30,7 @@ class Optional {
 	 * that has not been compiled into the code.
 	 * 
 	 * @author niki
-	 *
+	 * 
 	 */
 	public class NotSupportedException extends Exception {
 		private static final long serialVersionUID = 1L;
@@ -70,7 +71,7 @@ class Optional {
 	 * 
 	 * @param port
 	 *            the port to run on
-	 *
+	 * 
 	 * @throws NotSupportedException
 	 *             in case the option is not supported
 	 * @throws IOException
@@ -124,9 +125,10 @@ class Optional {
 			@SuppressWarnings("rawtypes")
 			Class launcherClass = Class
 					.forName("be.nikiroo.jvcard.tui.TuiLauncher");
-			Method start = launcherClass.getDeclaredMethod("start",
-					new Class<?>[] { Boolean.class, List.class });
-			start.invoke(launcherClass.newInstance(), textMode, files);
+			Constructor<?> cons = launcherClass.getConstructors()[0];
+			Object instance = cons.newInstance(textMode, files);
+			Method start = launcherClass.getDeclaredMethod("start");
+			start.invoke(instance);
 		} catch (NoSuchMethodException e) {
 			throw new Optional().new NotSupportedException(e, "TUI", true);
 		} catch (ClassNotFoundException e) {
@@ -140,6 +142,9 @@ class Optional {
 		} catch (IllegalArgumentException e) {
 			throw new Optional().new NotSupportedException(e, "TUI", false);
 		} catch (InvocationTargetException e) {
+			e.printStackTrace();
+			throw new Optional().new NotSupportedException(e, "TUI", false);
+		} catch (IndexOutOfBoundsException e) {
 			throw new Optional().new NotSupportedException(e, "TUI", false);
 		}
 	}
diff --git a/src/be/nikiroo/jvcard/tui/TuiLauncher.java b/src/be/nikiroo/jvcard/tui/TuiLauncher.java
index 054fe4a..52d244d 100644
--- a/src/be/nikiroo/jvcard/tui/TuiLauncher.java
+++ b/src/be/nikiroo/jvcard/tui/TuiLauncher.java
@@ -8,7 +8,6 @@ import be.nikiroo.jvcard.tui.panes.FileList;
 import com.googlecode.lanterna.TerminalSize;
 import com.googlecode.lanterna.TextColor;
 import com.googlecode.lanterna.gui2.MultiWindowTextGUI;
-import com.googlecode.lanterna.gui2.Window;
 import com.googlecode.lanterna.screen.Screen;
 import com.googlecode.lanterna.screen.TerminalScreen;
 import com.googlecode.lanterna.terminal.DefaultTerminalFactory;
@@ -19,13 +18,15 @@ import com.googlecode.lanterna.terminal.Terminal;
  * Starting the TUI.
  * 
  * @author niki
- *
+ * 
  */
 public class TuiLauncher {
-	static private Screen screen = null;
+	static private Screen screen;
+
+	private Boolean textMode;
+	private List<String> files;
 
 	/**
-	 * Start the TUI program.
 	 * 
 	 * @param textMode
 	 *            TRUE to force text mode, FALSE to force the Swing terminal
@@ -33,37 +34,21 @@ public class TuiLauncher {
 	 * @param files
 	 *            the files to show at startup
 	 * 
-	 * @throws IOException
-	 *             in case of IO error
-	 */
-	static public void start(Boolean textMode, List<String> files)
-			throws IOException {
-		Window win = new MainWindow(new FileList(files));
-		TuiLauncher.start(textMode, win);
-	}
-
-	/**
-	 * Return the used {@link Screen}.
-	 * 
-	 * @return the {@link Screen}
 	 */
-	static public Screen getScreen() {
-		return screen;
+	public TuiLauncher(Boolean textMode, List<String> files) {
+		this.textMode = textMode;
+		this.files = files;
 	}
 
 	/**
 	 * Start the TUI program.
 	 * 
-	 * @param textMode
-	 *            TRUE to force text mode, FALSE to force the Swing terminal
-	 *            emulator, null to automatically determine the best choice
-	 * @param win
-	 *            the window to show at start
+	 * 
 	 * 
 	 * @throws IOException
 	 *             in case of IO error
 	 */
-	static public void start(Boolean textMode, Window win) throws IOException {
+	public void start() throws IOException {
 		Terminal terminal = null;
 
 		DefaultTerminalFactory factory = new DefaultTerminalFactory();
@@ -76,16 +61,14 @@ public class TuiLauncher {
 			terminal = factory.createTerminalEmulator();
 		}
 
-		if (win instanceof MainWindow) {
-			final MainWindow mwin = (MainWindow) win;
-			mwin.refresh(terminal.getTerminalSize());
-			terminal.addResizeListener(new ResizeListener() {
-				@Override
-				public void onResized(Terminal terminal, TerminalSize newSize) {
-					mwin.refresh(newSize);
-				}
-			});
-		}
+		final MainWindow win = new MainWindow(new FileList(files));
+		win.refresh(terminal.getTerminalSize());
+		terminal.addResizeListener(new ResizeListener() {
+			@Override
+			public void onResized(Terminal terminal, TerminalSize newSize) {
+				win.refresh(newSize);
+			}
+		});
 
 		screen = new TerminalScreen(terminal);
 		screen.startScreen();
@@ -99,4 +82,13 @@ public class TuiLauncher {
 		gui.addWindowAndWait(win);
 		screen.stopScreen();
 	}
+
+	/**
+	 * Return the used {@link Screen}.
+	 * 
+	 * @return the {@link Screen}
+	 */
+	static public Screen getScreen() {
+		return screen;
+	}
 }
diff --git a/src/be/nikiroo/jvcard/tui/TuiLauncherJexer.java b/src/be/nikiroo/jvcard/tui/TuiLauncherJexer.java
new file mode 100644
index 0000000..445fc78
--- /dev/null
+++ b/src/be/nikiroo/jvcard/tui/TuiLauncherJexer.java
@@ -0,0 +1,74 @@
+package be.nikiroo.jvcard.tui;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.util.List;
+
+import jexer.TApplication;
+import jexer.TWindow;
+import be.nikiroo.jvcard.tui.windows.TuiFileListWindow;
+
+/**
+ * Starting the TUI.
+ * 
+ * @author niki
+ */
+public class TuiLauncherJexer extends TApplication {
+
+	/**
+	 * @param textMode
+	 *            TRUE to force text mode, FALSE to force the Swing terminal
+	 *            emulator, null to automatically determine the best choice
+	 * @param files
+	 *            the files to show at startup
+	 * @throws UnsupportedEncodingException
+	 */
+	public TuiLauncherJexer(final Boolean textMode, final List<String> files)
+			throws UnsupportedEncodingException {
+		super(backend(textMode));
+
+		addFileMenu();
+		addWindowMenu();
+
+		@SuppressWarnings("unused")
+		TWindow w = new TuiFileListWindow(this, files);
+	}
+
+	/**
+	 * Start the TUI program.
+	 * 
+	 * @throws IOException
+	 *             in case of IO error
+	 */
+	public void start() throws IOException {
+		(new Thread(this)).start();
+	}
+
+	/**
+	 * Select the most appropriate backend.
+	 * 
+	 * @param textMode
+	 *            NULL for auto-dection
+	 * @return the backend type to use
+	 */
+	private static BackendType backend(Boolean textMode) {
+		if (textMode == null) {
+			boolean isMsWindows = System.getProperty("os.name", "")
+					.toLowerCase().startsWith("windows");
+			boolean forceSwing = System.getProperty("jexer.Swing", "false")
+					.equals("true");
+			boolean noConsole = System.console() == null;
+			if (isMsWindows || forceSwing || noConsole) {
+				return BackendType.SWING;
+			}
+
+			return BackendType.XTERM;
+		}
+
+		if (textMode) {
+			return BackendType.XTERM;
+		}
+
+		return BackendType.SWING;
+	}
+}
diff --git a/src/be/nikiroo/jvcard/tui/windows/TuiBrowserWindow.java b/src/be/nikiroo/jvcard/tui/windows/TuiBrowserWindow.java
new file mode 100644
index 0000000..55cc4aa
--- /dev/null
+++ b/src/be/nikiroo/jvcard/tui/windows/TuiBrowserWindow.java
@@ -0,0 +1,119 @@
+package be.nikiroo.jvcard.tui.windows;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import jexer.TAction;
+import jexer.TApplication;
+import jexer.TKeypress;
+import jexer.TTable;
+import jexer.TWindow;
+import jexer.event.TKeypressEvent;
+import jexer.event.TResizeEvent;
+import be.nikiroo.jvcard.tui.panes.MainContent;
+
+public abstract class TuiBrowserWindow extends TWindow {
+	private TApplication app;
+	private TTable table;
+	private boolean showHeader;
+	private Map<TKeypress, TAction> keyBindings;
+
+	public TuiBrowserWindow(TApplication app, String title, boolean showHeaders) {
+		super(app, title, 10, 10);
+
+		this.app = app;
+		this.showHeader = showHeaders;
+
+		table = new TTable(this, 0, 0, getWidth(), getHeight(), new TAction() {
+			@Override
+			public void DO() {
+				onAction(table.getSelectedLine(), table.getSelectedColumn());
+			}
+		}, null);
+
+		keyBindings = new HashMap<TKeypress, TAction>();
+
+		// TODO: fullscreen selection?
+
+		// TODO: auto-maximize on FS, auto-resize on maximize
+		// setFullscreen(true);
+		maximize();
+		onResize(null);
+	}
+
+	/**
+	 * Change the currently displayed data.
+	 * 
+	 * @param headers
+	 *            the table headers (mandatory)
+	 * @param lines
+	 *            the data to display
+	 */
+	public void setData(List<String> headers, List<List<String>> lines) {
+		int prevLine = table.getSelectedLine();
+		int prevColumn = table.getSelectedColumn();
+
+		table.clear();
+		table.setHeaders(headers, showHeader);
+		for (List<String> line : lines) {
+			table.addLine(line);
+		}
+
+		table.reflow();
+
+		table.setSelectedLine(Math.min(prevLine, table.getNumberOfLines() - 1));
+		table.setSelectedColumn(Math.min(prevColumn,
+				table.getNumberOfColumns() - 1));
+	}
+
+	public void addKeyBinding(TKeypress key, TAction action) {
+		keyBindings.put(key, action);
+	}
+
+	/**
+	 * Return the number of items in this {@link MainContent}, or -1 if this
+	 * {@link MainContent} is not countable.
+	 * 
+	 * @return -1 or the number of present items
+	 */
+	public int size() {
+		return table.getNumberOfLines();
+	}
+
+	/**
+	 * Close the window.
+	 */
+	public void close() {
+		app.closeWindow(this);
+	}
+
+	/**
+	 * An item has been selected.
+	 * 
+	 * @param selectedLine
+	 *            the currently selected line
+	 * @param selectedColumn
+	 *            the currently selected column
+	 */
+	@SuppressWarnings("unused")
+	public void onAction(int selectedLine, int selectedColumn) {
+	}
+
+	@Override
+	public void onResize(TResizeEvent resize) {
+		super.onResize(resize);
+		table.setWidth(getWidth());
+		table.setHeight(getHeight());
+		table.reflow();
+	}
+
+	@Override
+	public void onKeypress(TKeypressEvent keypress) {
+		if (keyBindings.containsKey(keypress.getKey())) {
+			keyBindings.get(keypress.getKey()).DO();
+		} else {
+			super.onKeypress(keypress);
+		}
+	}
+}
diff --git a/src/be/nikiroo/jvcard/tui/windows/TuiContactListWindow.java b/src/be/nikiroo/jvcard/tui/windows/TuiContactListWindow.java
new file mode 100644
index 0000000..af6b08e
--- /dev/null
+++ b/src/be/nikiroo/jvcard/tui/windows/TuiContactListWindow.java
@@ -0,0 +1,110 @@
+package be.nikiroo.jvcard.tui.windows;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+
+import jexer.TAction;
+import jexer.TApplication;
+import jexer.TKeypress;
+import jexer.TWindow;
+import be.nikiroo.jvcard.Card;
+import be.nikiroo.jvcard.Contact;
+import be.nikiroo.jvcard.resources.DisplayBundle;
+import be.nikiroo.jvcard.resources.DisplayOption;
+
+public class TuiContactListWindow extends TuiBrowserWindow {
+	private TApplication app;
+	private Card card;
+	private String filter;
+	private List<String> formats;
+	private int selectedFormat;
+	private String format;
+
+	public TuiContactListWindow(TApplication app, Card card) {
+		super(app, "Contacts", false);
+
+		this.app = app;
+		this.card = card;
+		this.selectedFormat = -1;
+
+		DisplayBundle map = new DisplayBundle();
+		formats = new LinkedList<String>();
+		for (String format : map.getString(DisplayOption.CONTACT_LIST_FORMAT)
+				.split(",")) {
+			formats.add(format);
+		}
+
+		addKeyBinding(TKeypress.kbTab, new TAction() {
+			@Override
+			public void DO() {
+				switchFormat();
+			}
+		});
+
+		addKeyBinding(TKeypress.kbQ, new TAction() {
+			@Override
+			public void DO() {
+				close();
+			}
+		});
+
+		switchFormat();
+		setCard(card);
+	}
+
+	@Override
+	public void onAction(int selectedLine, int selectedColumn) {
+		try {
+			@SuppressWarnings("unused")
+			TWindow w = new TuiContactWindow(app, card.get(selectedLine));
+		} catch (IndexOutOfBoundsException e) {
+			setMessage("Fail to get contact", true);
+		}
+	}
+
+	private void setCard(Card card) {
+		List<String> headers = new ArrayList<String>();
+		for (String field : format.split("\\|")) {
+			headers.add(field);
+		}
+
+		List<List<String>> dataLines = new ArrayList<List<String>>();
+		if (card != null) {
+			for (Contact c : card) {
+				if (filter == null
+						|| c.toString(format, "|").toLowerCase()
+								.contains(filter.toLowerCase())) {
+					dataLines.add(Arrays.asList(c.toStringArray(format,
+							getWidth(), true)));
+				}
+			}
+		}
+
+		setData(headers, dataLines);
+	}
+
+	private void switchFormat() {
+		if (formats.size() == 0)
+			return;
+
+		selectedFormat++;
+		if (selectedFormat >= formats.size()) {
+			selectedFormat = 0;
+		}
+
+		format = formats.get(selectedFormat);
+
+		setCard(card);
+	}
+
+	// TODO
+	private void setMessage(String message, boolean error) {
+		if (error) {
+			System.err.println(message);
+		} else {
+			System.out.println(message);
+		}
+	}
+}
diff --git a/src/be/nikiroo/jvcard/tui/windows/TuiContactWindow.java b/src/be/nikiroo/jvcard/tui/windows/TuiContactWindow.java
new file mode 100644
index 0000000..bc4cb58
--- /dev/null
+++ b/src/be/nikiroo/jvcard/tui/windows/TuiContactWindow.java
@@ -0,0 +1,56 @@
+package be.nikiroo.jvcard.tui.windows;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import jexer.TAction;
+import jexer.TApplication;
+import jexer.TKeypress;
+import jexer.TLabel;
+import jexer.TWindow;
+import jexer.event.TKeypressEvent;
+import be.nikiroo.jvcard.Contact;
+
+public class TuiContactWindow extends TWindow {
+	private Map<TKeypress, TAction> keyBindings;
+
+	public TuiContactWindow(final TApplication app, final Contact contact) {
+		super(app, "Contact view", 40, 20);
+
+		keyBindings = new HashMap<TKeypress, TAction>();
+
+		keyBindings.put(TKeypress.kbQ, new TAction() {
+			@Override
+			public void DO() {
+				app.closeWindow(TuiContactWindow.this);
+			}
+		});
+
+		keyBindings.put(TKeypress.kbR, new TAction() {
+			@Override
+			public void DO() {
+				@SuppressWarnings("unused")
+				TWindow w = new TuiRawContactWindow(app, contact);
+			}
+		});
+
+		@SuppressWarnings("unused")
+		TLabel l = new TLabel(this, "'r' to see raw view", 0, 0);
+
+		// TODO: fullscreen selection?
+
+		// TODO: auto-maximize on FS, auto-resize on maximize
+		// setFullscreen(true);
+		maximize();
+		onResize(null);
+	}
+
+	@Override
+	public void onKeypress(TKeypressEvent keypress) {
+		if (keyBindings.containsKey(keypress.getKey())) {
+			keyBindings.get(keypress.getKey()).DO();
+		} else {
+			super.onKeypress(keypress);
+		}
+	}
+}
diff --git a/src/be/nikiroo/jvcard/tui/windows/TuiFileListWindow.java b/src/be/nikiroo/jvcard/tui/windows/TuiFileListWindow.java
new file mode 100644
index 0000000..f1feea5
--- /dev/null
+++ b/src/be/nikiroo/jvcard/tui/windows/TuiFileListWindow.java
@@ -0,0 +1,152 @@
+package be.nikiroo.jvcard.tui.windows;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+import jexer.TAction;
+import jexer.TApplication;
+import jexer.TKeypress;
+import jexer.TWindow;
+import be.nikiroo.jvcard.Card;
+import be.nikiroo.jvcard.launcher.CardResult;
+import be.nikiroo.jvcard.launcher.CardResult.MergeCallback;
+import be.nikiroo.jvcard.launcher.Main;
+import be.nikiroo.jvcard.parsers.Format;
+
+public class TuiFileListWindow extends TuiBrowserWindow {
+	private TApplication app;
+	private List<String> files;
+	private List<CardResult> cards;
+
+	public TuiFileListWindow(TApplication app, List<String> files) {
+		super(app, "Contacts", false);
+
+		this.app = app;
+		this.files = files;
+
+		cards = new ArrayList<CardResult>();
+		for (int i = 0; i < files.size(); i++) {
+			cards.add(null);
+		}
+
+		List<String> headers = new ArrayList<String>();
+		headers.add("File");
+		List<List<String>> dataLines = new ArrayList<List<String>>();
+		for (String file : files) {
+			List<String> listOfOneFile = new ArrayList<String>(1);
+			listOfOneFile.add(new File(file).getName());
+			dataLines.add(listOfOneFile);
+		}
+
+		addKeyBinding(TKeypress.kbQ, new TAction() {
+			@Override
+			public void DO() {
+				close();
+			}
+		});
+
+		setData(headers, dataLines);
+	}
+
+	@Override
+	public void onAction(int selectedLine, int selectedColumn) {
+		try {
+			@SuppressWarnings("unused")
+			TWindow w = new TuiContactListWindow(app, getCard(selectedLine));
+		} catch (IOException e) {
+			setMessage("Fail to get file: " + e.getMessage(), true);
+		}
+	}
+
+	private Card getCard(int index) throws IOException {
+		// TODO: check index?
+		if (cards.get(index) == null) {
+			String file = files.get(index);
+			CardResult cardResult = retrieveCardResult(file);
+			cards.set(index, cardResult);
+		}
+
+		return cards.get(index).getCard();
+	}
+
+	private CardResult retrieveCardResult(String file) throws IOException {
+		CardResult cardResult = null;
+		final Card arr[] = new Card[4];
+		try {
+			cardResult = Main.getCard(file, new MergeCallback() {
+				@Override
+				public Card merge(Card previous, Card local, Card server,
+						Card autoMerged) {
+					arr[0] = previous;
+					arr[1] = local;
+					arr[2] = server;
+					arr[3] = autoMerged;
+
+					return null;
+				}
+			});
+
+			cardResult.getCard(); // throw IOE if sync issues
+		} catch (IOException e) {
+			// Check if merge issue or something else I/O related
+			if (arr[0] == null)
+				throw e; // other I/O problems
+
+			// merge management: set all merge vars in
+			// merger,
+			// make sure it has cards but mergeTargetFile
+			// does not exist
+			// (create then delete if needed)
+			// TODO: i18n
+			setMessage("Merge error, please check/fix the merged contact", true);
+
+			// TODO: i18n + filename with numbers in it to
+			// fix
+			File a = File.createTempFile("Merge result ", ".vcf");
+			File p = File.createTempFile("Previous common version ", ".vcf");
+			File l = File.createTempFile("Local ", ".vcf");
+			File s = File.createTempFile("Remote ", ".vcf");
+			arr[3].saveAs(a, Format.VCard21);
+			arr[0].saveAs(p, Format.VCard21);
+			arr[1].saveAs(l, Format.VCard21);
+			arr[2].saveAs(s, Format.VCard21);
+			List<String> mfiles = new LinkedList<String>();
+			mfiles.add(a.getAbsolutePath());
+			mfiles.add(p.getAbsolutePath());
+			mfiles.add(l.getAbsolutePath());
+			mfiles.add(s.getAbsolutePath());
+			/*
+			 * merger = new FileList(mfiles); merger.mergeRemoteState =
+			 * arr[2].getContentState(false); merger.mergeSourceFile =
+			 * files.get(index); merger.mergeTargetFile = a;
+			 * 
+			 * obj = merger;
+			 */
+		}
+
+		// TODO:
+		// invalidate();
+
+		if (cardResult.isSynchronised()) {
+			// TODO i18n
+			if (cardResult.isChanged())
+				setMessage("card synchronised: changes from server", false);
+			else
+				setMessage("card synchronised: no changes", false);
+		}
+
+		return cardResult;
+	}
+
+	// TODO
+	private void setMessage(String message, boolean error) {
+		if (error) {
+			System.err.println(message);
+		} else {
+			System.out.println(message);
+		}
+	}
+}
diff --git a/src/be/nikiroo/jvcard/tui/windows/TuiRawContactWindow.java b/src/be/nikiroo/jvcard/tui/windows/TuiRawContactWindow.java
new file mode 100644
index 0000000..c784b80
--- /dev/null
+++ b/src/be/nikiroo/jvcard/tui/windows/TuiRawContactWindow.java
@@ -0,0 +1,41 @@
+package be.nikiroo.jvcard.tui.windows;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import jexer.TAction;
+import jexer.TApplication;
+import jexer.TKeypress;
+import be.nikiroo.jvcard.Contact;
+import be.nikiroo.jvcard.Data;
+
+public class TuiRawContactWindow extends TuiBrowserWindow {
+
+	public TuiRawContactWindow(TApplication app, Contact contact) {
+		super(app, "Contact RAW mode", false);
+
+		List<String> headers = new ArrayList<String>();
+		headers.add("Name");
+		headers.add("Value");
+		List<List<String>> dataLines = new ArrayList<List<String>>();
+		for (Data data : contact) {
+			List<String> dataLine = new ArrayList<String>(1);
+			dataLine.add(data.getName());
+			if (data.isBinary()) {
+				dataLine.add("[BINARY]");
+			} else {
+				dataLine.add(data.getValue());
+			}
+			dataLines.add(dataLine);
+		}
+
+		addKeyBinding(TKeypress.kbQ, new TAction() {
+			@Override
+			public void DO() {
+				close();
+			}
+		});
+
+		setData(headers, dataLines);
+	}
+}