From 296a0b75515b3a7424b98292c87cbbf2272b73f9 Mon Sep 17 00:00:00 2001 From: Niki Roo Date: Mon, 29 Feb 2016 11:35:59 +0100 Subject: [PATCH] Fix --noutf, fix onAction being called to many times, lot of small fixes --- src/be/nikiroo/jvcard/Card.java | 5 +- src/be/nikiroo/jvcard/Contact.java | 65 ++- src/be/nikiroo/jvcard/i18n/Trans.java | 76 ++- src/be/nikiroo/jvcard/tui/KeyAction.java | 4 +- src/be/nikiroo/jvcard/tui/Main.java | 8 +- src/be/nikiroo/jvcard/tui/MainWindow.java | 431 +++++++++--------- src/be/nikiroo/jvcard/tui/StringUtils.java | 82 +++- src/be/nikiroo/jvcard/tui/TuiLauncher.java | 18 +- src/be/nikiroo/jvcard/tui/UiColors.java | 28 +- ...actDetails.java => ContactDetailsRaw.java} | 8 +- .../nikiroo/jvcard/tui/panes/ContactList.java | 4 +- src/be/nikiroo/jvcard/tui/panes/FileList.java | 2 + .../jvcard/tui/panes/MainContentList.java | 118 +++-- .../lanterna/gui2/AbstractWindow.java | 2 +- 14 files changed, 520 insertions(+), 331 deletions(-) rename src/be/nikiroo/jvcard/tui/panes/{ContactDetails.java => ContactDetailsRaw.java} (92%) diff --git a/src/be/nikiroo/jvcard/Card.java b/src/be/nikiroo/jvcard/Card.java index e82ce7c..f3e0773 100644 --- a/src/be/nikiroo/jvcard/Card.java +++ b/src/be/nikiroo/jvcard/Card.java @@ -3,9 +3,11 @@ package be.nikiroo.jvcard; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; +import java.io.FileInputStream; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; +import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedList; @@ -36,7 +38,8 @@ public class Card { name = file.getName(); } - BufferedReader buffer = new BufferedReader(new FileReader(file)); + BufferedReader buffer = new BufferedReader(new InputStreamReader( + new FileInputStream(file), "UTF-8")); List lines = new LinkedList(); for (String line = buffer.readLine(); line != null; line = buffer .readLine()) { diff --git a/src/be/nikiroo/jvcard/Contact.java b/src/be/nikiroo/jvcard/Contact.java index 6436313..553ca76 100644 --- a/src/be/nikiroo/jvcard/Contact.java +++ b/src/be/nikiroo/jvcard/Contact.java @@ -7,6 +7,7 @@ import java.util.Map; import be.nikiroo.jvcard.parsers.Format; import be.nikiroo.jvcard.parsers.Parser; +import be.nikiroo.jvcard.tui.StringUtils; /** * A contact is the information that represent a contact person or organisation. @@ -155,7 +156,7 @@ public class Contact { * @return the {@link String} representation */ public String toString(String format) { - return toString(format, "|", null, -1); + return toString(format, "|", null, -1, true, false); } /** @@ -181,13 +182,17 @@ public class Contact { * @param width * a fixed width or -1 for "as long as needed" * + * @param unicode + * allow Uniode or only ASCII characters + * * @return the {@link String} representation */ public String toString(String format, String separator, String padding, - int width) { + int width, boolean unicode, boolean removeAccents) { StringBuilder builder = new StringBuilder(); - for (String str : toStringArray(format, separator, padding, width)) { + for (String str : toStringArray(format, separator, padding, width, + unicode)) { builder.append(str); } @@ -217,10 +222,13 @@ public class Contact { * @param width * a fixed width or -1 for "as long as needed" * + * @param unicode + * allow Uniode or only ASCII characters + * * @return the {@link String} representation */ public String[] toStringArray(String format, String separator, - String padding, int width) { + String padding, int width, boolean unicode) { if (width > -1) { int numOfFields = format.split("\\|").length; if (separator != null) @@ -235,7 +243,7 @@ public class Contact { List str = new LinkedList(); boolean first = true; - for (String s : toStringArray(format, width)) { + for (String s : toStringArray(format, width, unicode)) { if (!first) { str.add(separator); } @@ -269,10 +277,12 @@ public class Contact { * the format to use * @param width * a fixed width or -1 for "as long as needed" - * + * @param unicode + * allow Uniode or only ASCII characters + * * @return the {@link String} representation */ - public String[] toStringArray(String format, int width) { + public String[] toStringArray(String format, int width, boolean unicode) { List str = new LinkedList(); String[] formatFields = format.split("\\|"); @@ -318,11 +328,14 @@ public class Contact { } String value = getPreferredDataValue(field); - if (value == null) + if (value == null) { value = ""; + } else { + value = StringUtils.sanitize(value, unicode); + } if (size > -1) { - value = fixedString(value, size); + value = StringUtils.padString(value, size); } expandedFields[i] = expand; @@ -371,11 +384,13 @@ public class Contact { for (int i = 0; i < values.length; i++) { if (expandedFields[i]) { if (remainder > 0) { - values[i] = values[i] + fixedString("", remainder); + values[i] = values[i] + + StringUtils.padString("", remainder); remainder = 0; } if (padPerItem > 0) { - values[i] = values[i] + fixedString("", padPerItem); + values[i] = values[i] + + StringUtils.padString("", padPerItem); } } } @@ -392,30 +407,6 @@ public class Contact { return str.toArray(new String[] {}); } - /** - * Fix the size of the given {@link String} either with space-padding or by - * shortening it. - * - * @param string - * the {@link String} to fix - * @param size - * the size of the resulting {@link String} - * - * @return the fixed {@link String} of size size - */ - static private String fixedString(String string, int size) { - int length = string.length(); - - if (length > size) { - string = string.substring(0, size); - } else if (length < size) { - string = string - + new String(new char[size - length]).replace('\0', ' '); - } - - return string; - } - /** * Add a {@link String} to the given {@link List}, but make sure it does not * exceed the maximum size, and truncate it if needed to fit. @@ -532,7 +523,7 @@ public class Contact { data.setParent(this); } } - + /** * Delete this {@link Contact} from its parent {@link Card} if any. * @@ -559,7 +550,7 @@ public class Contact { */ void setPristine() { dirty = false; - for (Data data: datas) { + for (Data data : datas) { data.setPristine(); } } diff --git a/src/be/nikiroo/jvcard/i18n/Trans.java b/src/be/nikiroo/jvcard/i18n/Trans.java index 6030504..069f81c 100644 --- a/src/be/nikiroo/jvcard/i18n/Trans.java +++ b/src/be/nikiroo/jvcard/i18n/Trans.java @@ -3,6 +3,10 @@ package be.nikiroo.jvcard.i18n; import java.util.HashMap; import java.util.Map; +import com.googlecode.lanterna.input.KeyStroke; + +import be.nikiroo.jvcard.tui.UiColors; + /** * This class manages the translation of {@link Trans#StringId}s into * user-understandable text. @@ -27,6 +31,7 @@ public class Trans { KEY_ACTION_BACK, KEY_ACTION_HELP, // MainWindow KEY_ACTION_VIEW_CARD, // FileList KEY_ACTION_VIEW_CONTACT, KEY_ACTION_EDIT_CONTACT, KEY_ACTION_SAVE_CARD, KEY_ACTION_DELETE_CONTACT, KEY_ACTION_SWITCH_FORMAT, // ContactList + DEAULT_FIELD_SEPARATOR, DEAULT_FIELD_SEPARATOR_NOUTF, // MainContentList NULL; // Special usage public String trans() { @@ -48,12 +53,74 @@ public class Trans { return instance; } + /** + * Translate the given {@link StringId} into user text. + * + * @param stringId + * the ID to translate + * + * @return the translated text + */ public String trans(StringId stringId) { - if (map.containsKey(stringId)) { - return map.get(stringId); + StringId id = stringId; + if (!UiColors.getInstance().isUnicode()) { + try { + id = StringId.valueOf(stringId.toString() + "_NOUTF"); + } catch (IllegalArgumentException iae) { + // no special _NOUTF version found + } + } + + if (map.containsKey(id)) { + return map.get(id); + } + + return id.toString(); + } + + /** + * Translate the given {@link KeyStroke} into a user text {@link String} of + * size 3. + * + * @param key + * the key to translate + * + * @return the translated text + */ + public String trans(KeyStroke key) { + String keyTrans = ""; + + switch (key.getKeyType()) { + case Enter: + if (UiColors.getInstance().isUnicode()) + keyTrans = " ⤶ "; + else + keyTrans = "ENT"; + break; + case Tab: + if (UiColors.getInstance().isUnicode()) + keyTrans = " ↹ "; + else + keyTrans = "TAB"; + + break; + case Character: + keyTrans = " " + key.getCharacter() + " "; + break; + default: + keyTrans = "" + key.getKeyType(); + int width = 3; + if (keyTrans.length() > width) { + keyTrans = keyTrans.substring(0, width); + } else if (keyTrans.length() < width) { + keyTrans = keyTrans + + new String(new char[width - keyTrans.length()]) + .replace('\0', ' '); + } + break; } - return stringId.toString(); + return keyTrans; } private Trans() { @@ -62,6 +129,9 @@ public class Trans { // TODO: get from a file instead? map.put(StringId.NULL, ""); map.put(StringId.DUMMY, "[dummy]"); + // we could use: " ", "┃", "│"... + map.put(StringId.DEAULT_FIELD_SEPARATOR, "┃"); + map.put(StringId.DEAULT_FIELD_SEPARATOR_NOUTF, "|"); map.put(StringId.KEY_ACTION_BACK, "Back"); map.put(StringId.KEY_ACTION_HELP, "Help"); map.put(StringId.KEY_ACTION_VIEW_CONTACT, "Open"); diff --git a/src/be/nikiroo/jvcard/tui/KeyAction.java b/src/be/nikiroo/jvcard/tui/KeyAction.java index 70282d5..5686e2e 100644 --- a/src/be/nikiroo/jvcard/tui/KeyAction.java +++ b/src/be/nikiroo/jvcard/tui/KeyAction.java @@ -146,14 +146,14 @@ public class KeyAction { return null; } - // override this one if needed + // override this one if needed, DO NOT process here as it will be call a lot public Object getObject() { return null; } /** * The method which is called when the action is performed. You can subclass - * it if you want to customize the action (by default, it just accepts the + * it if you want to customise the action (by default, it just accepts the * mode change (see {@link KeyAction#getMode}). * * @return false to cancel mode change diff --git a/src/be/nikiroo/jvcard/tui/Main.java b/src/be/nikiroo/jvcard/tui/Main.java index a95e2e3..8e34e00 100644 --- a/src/be/nikiroo/jvcard/tui/Main.java +++ b/src/be/nikiroo/jvcard/tui/Main.java @@ -48,6 +48,8 @@ public class Main { + "\t--help: this here thingy\n" + "\t--tui: force pure text mode even if swing treminal is available\n" + "\t--gui: force swing terminal mode\n" + + "\t--noutf: force non-utf8 mode if you need it\n" + + "\t--noutfa: force non-utf8 and no accents mode if you need it\n" + "everyhing else is either a file to open or a directory to open\n" + "(we will only open 1st level files in given directories)"); return; @@ -55,6 +57,8 @@ public class Main { textMode = true; } else if (!noMoreParams && arg.equals("--gui")) { textMode = false; + } else if (!noMoreParams && arg.equals("--noutf")) { + UiColors.getInstance().setUnicode(false); } else { filesTried = true; files.addAll(open(arg)); @@ -70,8 +74,10 @@ public class Main { files.addAll(open(".")); } + Window win = new MainWindow(new FileList(files)); + try { - TuiLauncher.start(textMode, new MainWindow(new FileList(files))); + TuiLauncher.start(textMode, win); } catch (IOException ioe) { ioe.printStackTrace(); System.exit(2); diff --git a/src/be/nikiroo/jvcard/tui/MainWindow.java b/src/be/nikiroo/jvcard/tui/MainWindow.java index 459d1b1..13c6b3e 100644 --- a/src/be/nikiroo/jvcard/tui/MainWindow.java +++ b/src/be/nikiroo/jvcard/tui/MainWindow.java @@ -9,10 +9,11 @@ import java.util.List; import be.nikiroo.jvcard.Card; import be.nikiroo.jvcard.Contact; import be.nikiroo.jvcard.Data; +import be.nikiroo.jvcard.i18n.Trans; import be.nikiroo.jvcard.i18n.Trans.StringId; import be.nikiroo.jvcard.tui.KeyAction.Mode; import be.nikiroo.jvcard.tui.UiColors.Element; -import be.nikiroo.jvcard.tui.panes.ContactDetails; +import be.nikiroo.jvcard.tui.panes.ContactDetailsRaw; import be.nikiroo.jvcard.tui.panes.ContactList; import be.nikiroo.jvcard.tui.panes.MainContent; @@ -25,7 +26,6 @@ import com.googlecode.lanterna.gui2.Label; import com.googlecode.lanterna.gui2.LinearLayout; import com.googlecode.lanterna.gui2.Panel; import com.googlecode.lanterna.gui2.TextBox; -import com.googlecode.lanterna.gui2.TextGUIGraphics; import com.googlecode.lanterna.gui2.Window; import com.googlecode.lanterna.input.KeyStroke; import com.googlecode.lanterna.input.KeyType; @@ -42,8 +42,7 @@ public class MainWindow extends BasicWindow { private List actions = new LinkedList(); private List contentStack = new LinkedList(); private boolean waitForOneKeyAnswer; - private KeyStroke questionKey; // key that "asked" a question, and to replay - // later with an answer + private KeyAction questionAction; private String titleCache; private Panel titlePanel; private Panel mainPanel; @@ -51,7 +50,6 @@ public class MainWindow extends BasicWindow { private Panel actionPanel; private Panel messagePanel; private TextBox text; - private int width; /** * Create a new, empty window. @@ -69,8 +67,6 @@ public class MainWindow extends BasicWindow { public MainWindow(MainContent content) { super(content == null ? "" : content.getTitle()); - width = -1; - setHints(Arrays.asList(Window.Hint.FULL_SCREEN, Window.Hint.NO_DECORATIONS, Window.Hint.FIT_TERMINAL_WINDOW)); @@ -201,13 +197,15 @@ public class MainWindow extends BasicWindow { * {@link MainWindow#handleQuestion}. The user will be asked to enter some * answer and confirm with ENTER. * + * @param action + * the related action * @param question * the question to ask * @param initial * the initial answer if any (to be edited by the user) */ - public void setQuestion(KeyStroke key, String question, String initial) { - setQuestion(key, question, initial, false); + public void setQuestion(KeyAction action, String question, String initial) { + setQuestion(action, question, initial, false); } /** @@ -215,41 +213,21 @@ public class MainWindow extends BasicWindow { * {@link MainWindow#handleQuestion}. The user will be asked to hit one key * as an answer. * + * @param action + * the related action * @param question * the question to ask */ - public void setQuestion(KeyStroke key, String question) { - setQuestion(key, question, null, true); - } - - @Override - public void draw(TextGUIGraphics graphics) { - int width = graphics.getSize().getColumns(); - - if (width != this.width) { - this.width = width; - - setTitle(); - - if (actions != null) - setActions(new ArrayList(actions), false); - } - - super.draw(graphics); - } - - @Override - public void invalidate() { - super.invalidate(); - for (MainContent content : contentStack) { - content.invalidate(); - } + public void setQuestion(KeyAction action, String question) { + setQuestion(action, question, null, true); } /** * Show a question to the user and switch to "ask for answer" mode see * {@link MainWindow#handleQuestion}. * + * @param action + * the related action * @param question * the question to ask * @param initial @@ -258,9 +236,9 @@ public class MainWindow extends BasicWindow { * TRUE for a one-key answer, FALSE for a text answer validated * by ENTER */ - private void setQuestion(KeyStroke key, String question, String initial, + private void setQuestion(KeyAction action, String question, String initial, boolean oneKey) { - questionKey = key; + questionAction = action; waitForOneKeyAnswer = oneKey; messagePanel.removeAllComponents(); @@ -272,8 +250,8 @@ public class MainWindow extends BasicWindow { Label lbl = UiColors.Element.LINE_MESSAGE_QUESTION.createLabel(" " + question + " "); - text = new TextBox(new TerminalSize(width - lbl.getSize().getColumns(), - 1)); + text = new TextBox(new TerminalSize(getSize().getColumns() + - lbl.getSize().getColumns(), 1)); if (initial != null) text.setText(initial); @@ -289,9 +267,61 @@ public class MainWindow extends BasicWindow { text.takeFocus(); } + /** + * Refresh the window and the empty-space handling. You should call this + * method when the window size changed. + * + * @param size + * the new size of the window + */ + public void refresh(TerminalSize size) { + if (size == null) + return; + + if (getSize() == null || !getSize().equals(size)) + setSize(size); + + setTitle(); + + if (actions != null) + setActions(new ArrayList(actions), false); + + invalidate(); + } + + @Override + public void invalidate() { + super.invalidate(); + for (MainContent content : contentStack) { + content.invalidate(); + } + } + + @Override + public boolean handleInput(KeyStroke key) { + boolean handled = false; + + if (questionAction != null) { + String answer = handleQuestion(key); + if (answer != null) { + handled = true; + + handleAction(questionAction, answer); + questionAction = null; + } + } else { + handled = handleKey(key); + } + + if (!handled) + handled = super.handleInput(key); + + return handled; + } + /** * Actually set the title inside the window. Will also call - * {@link BasicWindow#setTitle} with the compuited parameters. + * {@link BasicWindow#setTitle} with the computed parameters. */ private void setTitle() { String prefix = " " + Main.APPLICATION_TITLE + " (version " @@ -311,6 +341,8 @@ public class MainWindow extends BasicWindow { if (title.length() > 0) { prefix = prefix + ": "; + title = StringUtils.sanitize(title, UiColors.getInstance() + .isUnicode()); } String countStr = ""; @@ -318,6 +350,11 @@ public class MainWindow extends BasicWindow { countStr = "[" + count + "]"; } + int width = -1; + if (getSize() != null) { + width = getSize().getColumns(); + } + if (width > 0) { int padding = width - prefix.length() - title.length() - countStr.length(); @@ -398,29 +435,7 @@ public class MainWindow extends BasicWindow { if (" ".equals(trans)) continue; - String keyTrans = ""; - switch (action.getKey().getKeyType()) { - case Enter: - keyTrans = " ⤶ "; - break; - case Tab: - keyTrans = " ↹ "; - break; - case Character: - keyTrans = " " + action.getKey().getCharacter() + " "; - break; - default: - keyTrans = "" + action.getKey().getKeyType(); - int width = 3; - if (keyTrans.length() > width) { - keyTrans = keyTrans.substring(0, width); - } else if (keyTrans.length() < width) { - keyTrans = keyTrans - + new String(new char[width - keyTrans.length()]) - .replace('\0', ' '); - } - break; - } + String keyTrans = Trans.getInstance().trans(action.getKey()); Panel kPane = new Panel(); LinearLayout layout = new LinearLayout(Direction.HORIZONTAL); @@ -435,10 +450,14 @@ public class MainWindow extends BasicWindow { } // fill with "desc" colour + int width = -1; + if (getSize() != null) { + width = getSize().getColumns(); + } + if (width > 0) { actionPanel.addComponent(UiColors.Element.ACTION_DESC .createLabel(StringUtils.padString("", width))); - } } @@ -487,178 +506,166 @@ public class MainWindow extends BasicWindow { * * @return if the window handled the input */ - private boolean handleInput(KeyStroke key, String answer) { + private boolean handleKey(KeyStroke key) { boolean handled = false; - // reset the message pane if no answers are pending - if (answer == null) { - if (setMessage(null, false)) - return true; - } + if (setMessage(null, false)) + return true; for (KeyAction action : actions) { if (!action.match(key)) continue; - MainContent content = getContent(); handled = true; if (action.onAction()) { - Card card = action.getCard(); - Contact contact = action.getContact(); - Data data = action.getData(); - - switch (action.getMode()) { - case MOVE: - int x = 0; - int y = 0; - - if (action.getKey().getKeyType() == KeyType.ArrowUp) - x = -1; - if (action.getKey().getKeyType() == KeyType.ArrowDown) - x = 1; - if (action.getKey().getKeyType() == KeyType.ArrowLeft) - y = -1; - if (action.getKey().getKeyType() == KeyType.ArrowRight) - y = 1; - - if (content != null) { - String err = content.move(x, y); - if (err != null) - setMessage(err, true); - } + handleAction(action, null); + } - break; - // mode with windows: - case CONTACT_LIST: - if (card != null) { - pushContent(new ContactList(card)); - } - break; - case CONTACT_DETAILS: - if (contact != null) { - pushContent(new ContactDetails(contact)); - } - break; - // mode interpreted by MainWindow: - case HELP: - // TODO - // setMessage("Help! I need somebody! Help!", false); - if (answer == null) { - setQuestion(key, "Test question?", "[initial]"); - } else { - setMessage("You answered: " + answer, false); - } + break; + } - break; - case BACK: - String warning = content.getExitWarning(); - if (warning != null) { - if (answer == null) { - setQuestion(key, warning); - } else { - setMessage(null, false); - if (answer.equalsIgnoreCase("y")) { - popContent(); - } - } - } else { + return handled; + } + + /** + * Handle the input in case of "normal" (not "ask for answer") mode. + * + * @param key + * the key that was pressed + * @param answer + * the answer given for this key + * + * @return if the window handled the input + */ + private void handleAction(KeyAction action, String answer) { + MainContent content = getContent(); + + Card card = action.getCard(); + Contact contact = action.getContact(); + Data data = action.getData(); + + switch (action.getMode()) { + case MOVE: + int x = 0; + int y = 0; + + if (action.getKey().getKeyType() == KeyType.ArrowUp) + x = -1; + if (action.getKey().getKeyType() == KeyType.ArrowDown) + x = 1; + if (action.getKey().getKeyType() == KeyType.ArrowLeft) + y = -1; + if (action.getKey().getKeyType() == KeyType.ArrowRight) + y = 1; + + if (content != null) { + String err = content.move(x, y); + if (err != null) + setMessage(err, true); + } + + break; + // mode with windows: + case CONTACT_LIST: + if (card != null) { + pushContent(new ContactList(card)); + } + break; + case CONTACT_DETAILS: + if (contact != null) { + pushContent(new ContactDetailsRaw(contact)); + } + break; + // mode interpreted by MainWindow: + case HELP: + // TODO + // setMessage("Help! I need somebody! Help!", false); + if (answer == null) { + setQuestion(action, "Test question?", "[initial]"); + } else { + setMessage("You answered: " + answer, false); + } + + break; + case BACK: + String warning = content.getExitWarning(); + if (warning != null) { + if (answer == null) { + setQuestion(action, warning); + } else { + setMessage(null, false); + if (answer.equalsIgnoreCase("y")) { popContent(); } + } + } else { + popContent(); + } - if (contentStack.size() == 0) { - close(); - } + if (contentStack.size() == 0) { + close(); + } - break; - // action modes: - case EDIT_DETAIL: - if (answer == null) { - if (data != null) { - String name = data.getName(); - String value = data.getValue(); - setQuestion(key, name, value); - } + break; + // action modes: + case EDIT_DETAIL: + if (answer == null) { + if (data != null) { + String name = data.getName(); + String value = data.getValue(); + setQuestion(action, name, value); + } + } else { + setMessage(null, false); + data.setValue(answer); + } + break; + case DELETE_CONTACT: + if (answer == null) { + if (contact != null) { + setQuestion(action, "Delete contact? [Y/N]"); + } + } else { + setMessage(null, false); + if (answer.equalsIgnoreCase("y")) { + if (contact.delete()) { + content.refreshData(); + invalidate(); + setTitle(); } else { - setMessage(null, false); - data.setValue(answer); + setMessage("Cannot delete this contact", true); } - break; - case DELETE_CONTACT: - if (answer == null) { - if (contact != null) { - setQuestion(key, "Delete contact? [Y/N]"); - } - } else { - setMessage(null, false); - if (answer.equalsIgnoreCase("y")) { - if (contact.delete()) { - content.refreshData(); - invalidate(); - setTitle(); - } else { - setMessage("Cannot delete this contact", true); - } + } + } + break; + case SAVE_CARD: + if (answer == null) { + if (card != null) { + setQuestion(action, "Save changes? [Y/N]"); + } + } else { + setMessage(null, false); + if (answer.equalsIgnoreCase("y")) { + boolean ok = false; + try { + if (card.save()) { + ok = true; + invalidate(); } + } catch (IOException ioe) { + ioe.printStackTrace(); } - break; - case SAVE_CARD: - if (answer == null) { - if (card != null) { - setQuestion(key, "Save changes? [Y/N]"); - } - } else { - setMessage(null, false); - if (answer.equalsIgnoreCase("y")) { - boolean ok = false; - try { - if (card.save()) { - ok = true; - invalidate(); - } - } catch (IOException ioe) { - ioe.printStackTrace(); - } - - if (!ok) { - setMessage("Cannot save to file", true); - } - } + + if (!ok) { + setMessage("Cannot save to file", true); } - break; - default: - case NONE: - break; } } - + break; + default: + case NONE: break; } - - return handled; - } - - @Override - public boolean handleInput(KeyStroke key) { - boolean handled = false; - - if (questionKey != null) { - String answer = handleQuestion(key); - if (answer != null) { - handled = true; - - key = questionKey; - questionKey = null; - - handleInput(key, answer); - } - } else { - handled = handleInput(key, null); - } - - if (!handled) - handled = super.handleInput(key); - - return handled; } } diff --git a/src/be/nikiroo/jvcard/tui/StringUtils.java b/src/be/nikiroo/jvcard/tui/StringUtils.java index e9353c8..3f5d22b 100644 --- a/src/be/nikiroo/jvcard/tui/StringUtils.java +++ b/src/be/nikiroo/jvcard/tui/StringUtils.java @@ -1,14 +1,49 @@ package be.nikiroo.jvcard.tui; +import java.text.Normalizer; +import java.text.Normalizer.Form; +import java.util.regex.Pattern; + import com.googlecode.lanterna.gui2.LinearLayout.Alignment; public class StringUtils { + static private Pattern marks = Pattern + .compile("[\\p{InCombiningDiacriticalMarks}\\p{IsLm}\\p{IsSk}]+"); + static private Pattern notAscii = Pattern.compile("[^\\p{ASCII}]+"); + /** + * Fix the size of the given {@link String} either with space-padding or by + * shortening it. + * + * @param text + * the {@link String} to fix + * @param width + * the size of the resulting {@link String} if the text fits or + * if cut is TRUE + * + * @return the resulting {@link String} of size size + */ static public String padString(String text, int width) { return padString(text, width, true, Alignment.Beginning); } - // TODO: doc it, width of -1 == no change to text + /** + * Fix the size of the given {@link String} either with space-padding or by + * optionally shortening it. + * + * @param text + * the {@link String} to fix + * @param width + * the size of the resulting {@link String} if the text fits or + * if cut is TRUE + * @param cut + * cut the {@link String} shorter if needed + * @param align + * align the {@link String} in this position if we have enough + * space + * + * @return the resulting {@link String} of size size minimum + */ static public String padString(String text, int width, boolean cut, Alignment align) { @@ -47,4 +82,49 @@ public class StringUtils { return text; } + /** + * Sanitise the given input to make it more Terminal-friendly by removing + * combining characters. + * + * @param input + * the input to sanitise + * @param allowUnicode + * allow Unicode or only allow ASCII Latin characters + * + * @return the sanitised {@link String} + */ + static public String sanitize(String input, boolean allowUnicode) { + return sanitize(input, allowUnicode, !allowUnicode); + } + + /** + * Sanitise the given input to make it more Terminal-friendly by removing + * combining characters. + * + * @param input + * the input to sanitise + * @param allowUnicode + * allow Unicode or only allow ASCII Latin characters + * @param removeAllAccents + * TRUE to replace all accentuated characters by their non + * accentuated counter-parts + * + * @return the sanitised {@link String} + */ + static public String sanitize(String input, boolean allowUnicode, + boolean removeAllAccents) { + + if (removeAllAccents) { + input = Normalizer.normalize(input, Form.NFKD); + input = marks.matcher(input).replaceAll(""); + } + + input = Normalizer.normalize(input, Form.NFKC); + + if (!allowUnicode) { + input = notAscii.matcher(input).replaceAll(""); + } + + return input; + } } diff --git a/src/be/nikiroo/jvcard/tui/TuiLauncher.java b/src/be/nikiroo/jvcard/tui/TuiLauncher.java index efaa689..7bee3e7 100644 --- a/src/be/nikiroo/jvcard/tui/TuiLauncher.java +++ b/src/be/nikiroo/jvcard/tui/TuiLauncher.java @@ -2,6 +2,7 @@ package be.nikiroo.jvcard.tui; import java.io.IOException; +import com.googlecode.lanterna.TerminalSize; import com.googlecode.lanterna.TextColor; import com.googlecode.lanterna.gui2.DefaultWindowManager; import com.googlecode.lanterna.gui2.EmptySpace; @@ -10,11 +11,12 @@ import com.googlecode.lanterna.gui2.Window; import com.googlecode.lanterna.screen.Screen; import com.googlecode.lanterna.screen.TerminalScreen; import com.googlecode.lanterna.terminal.DefaultTerminalFactory; +import com.googlecode.lanterna.terminal.ResizeListener; import com.googlecode.lanterna.terminal.Terminal; /* * - * Change in Lanterna3 (issue and fix reported to GitHub): + * Change in Lanterna 3.0.0-beta2 (issue and fix reported to GitHub): * * java.lang.StringIndexOutOfBoundsException: String index out of range: 83 * at java.lang.String.charAt(String.java:686) @@ -24,8 +26,7 @@ import com.googlecode.lanterna.terminal.Terminal; */ public class TuiLauncher { - public static void start(Boolean textMode, Window win) - throws IOException { + public static void start(Boolean textMode, Window win) throws IOException { Terminal terminal = null; DefaultTerminalFactory factory = new DefaultTerminalFactory(); @@ -38,6 +39,17 @@ public class TuiLauncher { terminal = factory.createTerminalEmulator(); } + if (win instanceof MainWindow) { + MainWindow mwin = (MainWindow) win; + mwin.refresh(terminal.getTerminalSize()); + terminal.addResizeListener(new ResizeListener() { + @Override + public void onResized(Terminal terminal, TerminalSize newSize) { + mwin.refresh(newSize); + } + }); + } + Screen screen = new TerminalScreen(terminal); screen.startScreen(); diff --git a/src/be/nikiroo/jvcard/tui/UiColors.java b/src/be/nikiroo/jvcard/tui/UiColors.java index 834641d..589579a 100644 --- a/src/be/nikiroo/jvcard/tui/UiColors.java +++ b/src/be/nikiroo/jvcard/tui/UiColors.java @@ -18,6 +18,7 @@ public class UiColors { private Map mapForegroundColor = null; private Map mapBackgroundColor = null; + private boolean utf = true; /** * Get the (unique) instance of this class. @@ -34,10 +35,10 @@ public class UiColors { } public enum Element { - DEFAULT, // + DEFAULT, // TITLE_MAIN, TITLE_VARIABLE, TITLE_COUNT, // ACTION_KEY, ACTION_DESC, // - LINE_MESSAGE, LINE_MESSAGE_ERR, LINE_MESSAGE_QUESTION, LINE_MESSAGE_ANS, // + LINE_MESSAGE, LINE_MESSAGE_ERR, LINE_MESSAGE_QUESTION, LINE_MESSAGE_ANS, // CONTACT_LINE, CONTACT_LINE_SEPARATOR, CONTACT_LINE_SELECTED, CONTACT_LINE_SEPARATOR_SELECTED, CONTACT_LINE_DIRTY, CONTACT_LINE_DIRTY_SELECTED; /** @@ -67,6 +68,25 @@ public class UiColors { } } + /** + * Check if unicode characters should be used. + * + * @return TRUE to allow unicode + */ + public boolean isUnicode() { + return utf; + } + + /** + * Allow or disallow unicode characters in the program. + * + * @param utf + * TRUE to allow unuciode, FALSE to only allow ASCII characters + */ + public void setUnicode(boolean utf) { + this.utf = utf; + } + private Label createLabel(Element el, String text) { Label lbl = new Label(text); themeLabel(el, lbl); @@ -117,8 +137,7 @@ public class UiColors { addEl(Element.LINE_MESSAGE_ANS, TextColor.ANSI.BLUE, TextColor.ANSI.BLACK); addEl(Element.TITLE_MAIN, TextColor.ANSI.WHITE, TextColor.ANSI.BLUE); - addEl(Element.TITLE_VARIABLE, TextColor.ANSI.GREEN, - TextColor.ANSI.BLUE); + addEl(Element.TITLE_VARIABLE, TextColor.ANSI.GREEN, TextColor.ANSI.BLUE); addEl(Element.TITLE_COUNT, TextColor.ANSI.RED, TextColor.ANSI.BLUE); } @@ -126,5 +145,4 @@ public class UiColors { mapForegroundColor.put(el, fore); mapBackgroundColor.put(el, back); } - } diff --git a/src/be/nikiroo/jvcard/tui/panes/ContactDetails.java b/src/be/nikiroo/jvcard/tui/panes/ContactDetailsRaw.java similarity index 92% rename from src/be/nikiroo/jvcard/tui/panes/ContactDetails.java rename to src/be/nikiroo/jvcard/tui/panes/ContactDetailsRaw.java index 1fad960..506eeff 100644 --- a/src/be/nikiroo/jvcard/tui/panes/ContactDetails.java +++ b/src/be/nikiroo/jvcard/tui/panes/ContactDetailsRaw.java @@ -10,16 +10,17 @@ import be.nikiroo.jvcard.Data; import be.nikiroo.jvcard.TypeInfo; import be.nikiroo.jvcard.i18n.Trans; import be.nikiroo.jvcard.tui.KeyAction; +import be.nikiroo.jvcard.tui.UiColors; import be.nikiroo.jvcard.tui.KeyAction.DataType; import be.nikiroo.jvcard.tui.KeyAction.Mode; import be.nikiroo.jvcard.tui.StringUtils; import be.nikiroo.jvcard.tui.UiColors.Element; -public class ContactDetails extends MainContentList { +public class ContactDetailsRaw extends MainContentList { private Contact contact; private int mode; - public ContactDetails(Contact contact) { + public ContactDetailsRaw(Contact contact) { super(null, null); this.contact = contact; @@ -79,6 +80,9 @@ public class ContactDetails extends MainContentList { value = valueBuilder.toString(); + name = StringUtils.sanitize(name, UiColors.getInstance().isUnicode()); + value = StringUtils.sanitize(value, UiColors.getInstance().isUnicode()); + name = StringUtils.padString(name, SIZE_COL_1); value = StringUtils.padString(value, width - SIZE_COL_1 - getSeparator().length() - 2); diff --git a/src/be/nikiroo/jvcard/tui/panes/ContactList.java b/src/be/nikiroo/jvcard/tui/panes/ContactList.java index d5cf26b..7d10370 100644 --- a/src/be/nikiroo/jvcard/tui/panes/ContactList.java +++ b/src/be/nikiroo/jvcard/tui/panes/ContactList.java @@ -73,7 +73,7 @@ public class ContactList extends MainContentList { public List getKeyBindings() { List actions = new LinkedList(); - // TODO add, del, save... + // TODO add actions.add(new KeyAction(Mode.CONTACT_DETAILS, 'e', Trans.StringId.KEY_ACTION_EDIT_CONTACT) { @Override @@ -155,7 +155,7 @@ public class ContactList extends MainContentList { width -= 2; // dirty mark space String[] array = contact.toStringArray(format, getSeparator(), " ", - width); + width, UiColors.getInstance().isUnicode()); if (contact.isDirty()) { parts.add(new TextPart(" ", el)); diff --git a/src/be/nikiroo/jvcard/tui/panes/FileList.java b/src/be/nikiroo/jvcard/tui/panes/FileList.java index dfc3b73..66e35ce 100644 --- a/src/be/nikiroo/jvcard/tui/panes/FileList.java +++ b/src/be/nikiroo/jvcard/tui/panes/FileList.java @@ -72,6 +72,8 @@ public class FileList extends MainContentList { String name = files.get(index).getName(); + name = StringUtils.sanitize(name, UiColors.getInstance().isUnicode()); + count = " " + StringUtils.padString(count, SIZE_COL_1) + " "; name = " " + StringUtils.padString(name, width - SIZE_COL_1 diff --git a/src/be/nikiroo/jvcard/tui/panes/MainContentList.java b/src/be/nikiroo/jvcard/tui/panes/MainContentList.java index 38b775c..cac03e2 100644 --- a/src/be/nikiroo/jvcard/tui/panes/MainContentList.java +++ b/src/be/nikiroo/jvcard/tui/panes/MainContentList.java @@ -3,15 +3,17 @@ package be.nikiroo.jvcard.tui.panes; import java.util.LinkedList; import java.util.List; +import be.nikiroo.jvcard.i18n.Trans.StringId; +import be.nikiroo.jvcard.tui.StringUtils; import be.nikiroo.jvcard.tui.UiColors; import be.nikiroo.jvcard.tui.UiColors.Element; import com.googlecode.lanterna.TextColor; +import com.googlecode.lanterna.gui2.AbstractListBox.ListItemRenderer; import com.googlecode.lanterna.gui2.ActionListBox; import com.googlecode.lanterna.gui2.Direction; import com.googlecode.lanterna.gui2.LinearLayout; import com.googlecode.lanterna.gui2.TextGUIGraphics; -import com.googlecode.lanterna.gui2.AbstractListBox.ListItemRenderer; abstract public class MainContentList extends MainContent implements Runnable { private ActionListBox lines; @@ -59,62 +61,58 @@ abstract public class MainContentList extends MainContent implements Runnable { lines = new ActionListBox(); - lines - .setListItemRenderer(new ListItemRenderer() { - /** - * This is the main drawing method for a single list box - * item, it applies the current theme to setup the colors - * and then calls {@code getLabel(..)} and draws the result - * using the supplied {@code TextGUIGraphics}. The graphics - * object is created just for this item and is restricted so - * that it can only draw on the area this item is occupying. - * The top-left corner (0x0) should be the starting point - * when drawing the item. - * - * @param graphics - * Graphics object to draw with - * @param listBox - * List box we are drawing an item from - * @param index - * Index of the item we are drawing - * @param item - * The item we are drawing - * @param selected - * Will be set to {@code true} if the item is - * currently selected, otherwise {@code false}, - * but please notice what context 'selected' - * refers to here (see {@code setSelectedIndex}) - * @param focused - * Will be set to {@code true} if the list box - * currently has input focus, otherwise {@code - * false} - */ - public void drawItem(TextGUIGraphics graphics, - ActionListBox listBox, int index, Runnable item, - boolean selected, boolean focused) { - - // width "-1" to reserve space for the optional vertical - // scroll bar - List parts = MainContentList.this.getLabel( - index, lines.getSize().getColumns() - 1, - selected, focused); - - int position = 0; - for (TextPart part : parts) { - graphics.setForegroundColor(part - .getForegroundColor()); - graphics.setBackgroundColor(part - .getBackgroundColor()); - String label = part.getText(); - - graphics.putString(position, 0, label); - position += label.length(); - } - } - }); - - addComponent(lines, LinearLayout - .createLayoutData(LinearLayout.Alignment.Fill)); + lines.setListItemRenderer(new ListItemRenderer() { + /** + * This is the main drawing method for a single list box item, it + * applies the current theme to setup the colors and then calls + * {@code getLabel(..)} and draws the result using the supplied + * {@code TextGUIGraphics}. The graphics object is created just for + * this item and is restricted so that it can only draw on the area + * this item is occupying. The top-left corner (0x0) should be the + * starting point when drawing the item. + * + * @param graphics + * Graphics object to draw with + * @param listBox + * List box we are drawing an item from + * @param index + * Index of the item we are drawing + * @param item + * The item we are drawing + * @param selected + * Will be set to {@code true} if the item is currently + * selected, otherwise {@code false}, but please notice + * what context 'selected' refers to here (see + * {@code setSelectedIndex}) + * @param focused + * Will be set to {@code true} if the list box currently + * has input focus, otherwise {@code false} + */ + public void drawItem(TextGUIGraphics graphics, + ActionListBox listBox, int index, Runnable item, + boolean selected, boolean focused) { + + // width "-1" to reserve space for the optional vertical + // scroll bar + List parts = MainContentList.this.getLabel(index, + lines.getSize().getColumns() - 1, selected, focused); + + int position = 0; + for (TextPart part : parts) { + graphics.setForegroundColor(part.getForegroundColor()); + graphics.setBackgroundColor(part.getBackgroundColor()); + + String label = StringUtils.sanitize(part.getText(), + UiColors.getInstance().isUnicode()); + + graphics.putString(position, 0, label); + position += label.length(); + } + } + }); + + addComponent(lines, + LinearLayout.createLayoutData(LinearLayout.Alignment.Fill)); } /** @@ -152,16 +150,14 @@ abstract public class MainContentList extends MainContent implements Runnable { public void setSelectedIndex(int index) { lines.setSelectedIndex(index); } - - + /** * Return the default content separator for text fields. * * @return the separator */ public String getSeparator() { - // we could use: " ", "┃", "│"... - return "┃"; + return StringId.DEAULT_FIELD_SEPARATOR.trans(); } @Override diff --git a/src/com/googlecode/lanterna/gui2/AbstractWindow.java b/src/com/googlecode/lanterna/gui2/AbstractWindow.java index 4e55f3d..6c9c5fd 100644 --- a/src/com/googlecode/lanterna/gui2/AbstractWindow.java +++ b/src/com/googlecode/lanterna/gui2/AbstractWindow.java @@ -123,7 +123,7 @@ public abstract class AbstractWindow extends AbstractBasePane implements Window getComponent().invalidate(); } setSize(graphics.getSize(), false); - super.draw(graphics); + super.draw(graphics); } @Override -- 2.27.0