New launcher class to start all 3 modes:
[jvcard.git] / src / be / nikiroo / jvcard / tui / MainWindow.java
index a09751ef26a81dbbfa3f00c1e869fd9757ca05d9..236289904a07297401e35754a8f0d1babc98e12e 100644 (file)
@@ -1,30 +1,29 @@
 package be.nikiroo.jvcard.tui;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.LinkedList;
 import java.util.List;
 
-import be.nikiroo.jvcard.Card;
-import be.nikiroo.jvcard.Contact;
-import be.nikiroo.jvcard.i18n.Trans.StringId;
+import be.nikiroo.jvcard.launcher.Main;
+import be.nikiroo.jvcard.resources.StringUtils;
+import be.nikiroo.jvcard.resources.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;
 
 import com.googlecode.lanterna.TerminalSize;
-import com.googlecode.lanterna.TextColor;
 import com.googlecode.lanterna.gui2.BasicWindow;
 import com.googlecode.lanterna.gui2.BorderLayout;
-import com.googlecode.lanterna.gui2.ComponentRenderer;
 import com.googlecode.lanterna.gui2.Direction;
 import com.googlecode.lanterna.gui2.Interactable;
 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;
@@ -36,15 +35,13 @@ import com.googlecode.lanterna.input.KeyType;
  * @author niki
  * 
  */
+
 public class MainWindow extends BasicWindow {
        private List<KeyAction> defaultActions = new LinkedList<KeyAction>();
        private List<KeyAction> actions = new LinkedList<KeyAction>();
        private List<MainContent> contentStack = new LinkedList<MainContent>();
-       private boolean actionsPadded;
-       private boolean waitForOneKeyAnswer;
-       private KeyStroke questionKey; // key that "asked" a question, and to replay
-       // later with an answer
-       private String title;
+       private UserQuestion userQuestion;
+       private String titleCache;
        private Panel titlePanel;
        private Panel mainPanel;
        private Panel contentPanel;
@@ -52,6 +49,68 @@ public class MainWindow extends BasicWindow {
        private Panel messagePanel;
        private TextBox text;
 
+       /**
+        * Information about a question to ask the user and its answer.
+        * 
+        * @author niki
+        *
+        */
+       private class UserQuestion {
+               private boolean oneKeyAnswer;
+               private KeyAction action;
+               private String answer;
+
+               /**
+                * Create a new {@link UserQuestion}.
+                * 
+                * @param action
+                *            the action that triggered the question
+                * @param oneKeyAnswer
+                *            TRUE if we expect a one-key answer
+                */
+               public UserQuestion(KeyAction action, boolean oneKeyAnswer) {
+                       this.action = action;
+                       this.oneKeyAnswer = oneKeyAnswer;
+               }
+
+               /**
+                * Return the {@link KeyAction} that triggered the question.
+                * 
+                * @return the {@link KeyAction}
+                */
+               public KeyAction getAction() {
+                       return action;
+               }
+
+               /**
+                * Check if a one-key answer is expected.
+                * 
+                * @return TRUE if a one-key answer is expected
+                */
+               public boolean isOneKeyAnswer() {
+                       return oneKeyAnswer;
+               }
+
+               /**
+                * Return the user answer.
+                * 
+                * @return the user answer
+                */
+               public String getAnswer() {
+                       return answer;
+               }
+
+               /**
+                * Set the user answer.
+                * 
+                * @param answer
+                *            the new answer
+                */
+               public void setAnswer(String answer) {
+                       this.answer = answer;
+               }
+       }
+
        /**
         * Create a new, empty window.
         */
@@ -130,11 +189,9 @@ public class MainWindow extends BasicWindow {
         */
        public void pushContent(MainContent content) {
                List<KeyAction> actions = null;
-               String title = null;
 
                contentPanel.removeAllComponents();
                if (content != null) {
-                       title = content.getTitle();
                        actions = content.getKeyBindings();
                        contentPanel.addComponent(content, BorderLayout.Location.CENTER);
                        this.contentStack.add(content);
@@ -144,10 +201,8 @@ public class MainWindow extends BasicWindow {
                                focus.takeFocus();
                }
 
-               setTitle(title);
+               setTitle();
                setActions(actions, true);
-
-               invalidate();
        }
 
        /**
@@ -178,31 +233,72 @@ public class MainWindow extends BasicWindow {
         *            the message to display
         * @param error
         *            TRUE for an error message, FALSE for an information message
+        * 
+        * @return TRUE if changes were performed
         */
-       public void setMessage(String mess, boolean error) {
-               messagePanel.removeAllComponents();
-               if (mess != null) {
-                       Element element = (error ? UiColors.Element.LINE_MESSAGE_ERR
-                                       : UiColors.Element.LINE_MESSAGE);
-                       Label lbl = element.createLabel(" " + mess + " ");
-                       messagePanel.addComponent(lbl, LinearLayout
-                                       .createLayoutData(LinearLayout.Alignment.Center));
+       public boolean setMessage(String mess, boolean error) {
+               if (mess != null || messagePanel.getChildCount() > 0) {
+                       messagePanel.removeAllComponents();
+                       if (mess != null) {
+                               Element element = (error ? UiColors.Element.LINE_MESSAGE_ERR
+                                               : UiColors.Element.LINE_MESSAGE);
+                               Label lbl = element.createLabel(" " + mess + " ");
+                               messagePanel.addComponent(lbl, LinearLayout
+                                               .createLayoutData(LinearLayout.Alignment.Center));
+                       }
+                       return true;
                }
+
+               return false;
+       }
+
+       /**
+        * Show a question to the user and switch to "ask for answer" mode see
+        * {@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(KeyAction action, String question, String initial) {
+               setQuestion(action, question, initial, false);
+       }
+
+       /**
+        * Show a question to the user and switch to "ask for answer" mode see
+        * {@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(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 mess
-        *            the message to display
+        * @param action
+        *            the related action
+        * @param question
+        *            the question to ask
+        * @param initial
+        *            the initial answer if any (to be edited by the user)
         * @param oneKey
         *            TRUE for a one-key answer, FALSE for a text answer validated
         *            by ENTER
         */
-       public void setQuestion(KeyStroke key, String mess, boolean oneKey) {
-               questionKey = key;
-               waitForOneKeyAnswer = oneKey;
+       private void setQuestion(KeyAction action, String question, String initial,
+                       boolean oneKey) {
+               userQuestion = new UserQuestion(action, oneKey);
 
                messagePanel.removeAllComponents();
 
@@ -212,72 +308,149 @@ public class MainWindow extends BasicWindow {
                hpanel.setLayoutManager(llayout);
 
                Label lbl = UiColors.Element.LINE_MESSAGE_QUESTION.createLabel(" "
-                               + mess + " ");
+                               + question + " ");
                text = new TextBox(new TerminalSize(getSize().getColumns()
                                - lbl.getSize().getColumns(), 1));
 
-               hpanel.addComponent(lbl, LinearLayout
-                               .createLayoutData(LinearLayout.Alignment.Beginning));
-               hpanel.addComponent(text, LinearLayout
-                               .createLayoutData(LinearLayout.Alignment.Fill));
+               if (initial != null) {
+                       // add all chars one by one so the caret is at the end
+                       for (int index = 0; index < initial.length(); index++) {
+                               text.handleInput(new KeyStroke(initial.charAt(index), false,
+                                               false));
+                       }
+               }
+
+               hpanel.addComponent(lbl,
+                               LinearLayout.createLayoutData(LinearLayout.Alignment.Beginning));
+               hpanel.addComponent(text,
+                               LinearLayout.createLayoutData(LinearLayout.Alignment.Fill));
 
-               messagePanel.addComponent(hpanel, LinearLayout
-                               .createLayoutData(LinearLayout.Alignment.Beginning));
+               messagePanel
+                               .addComponent(hpanel, LinearLayout
+                                               .createLayoutData(LinearLayout.Alignment.Beginning));
 
                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<KeyAction>(actions), false);
+
+               invalidate();
+       }
+
        @Override
-       public void draw(TextGUIGraphics graphics) {
-               if (!actionsPadded) {
-                       // fill with "desc" colour
-                       actionPanel.addComponent(UiColors.Element.ACTION_DESC
-                                       .createLabel(StringUtils.padString("", graphics.getSize()
-                                                       .getColumns())));
-                       actionsPadded = true;
+       public void invalidate() {
+               super.invalidate();
+               for (MainContent content : contentStack) {
+                       content.invalidate();
                }
-               super.draw(graphics);
        }
 
        @Override
-       public void setTitle(String title) {
+       public boolean handleInput(KeyStroke key) {
+               boolean handled = false;
+
+               if (userQuestion != null) {
+                       handled = handleQuestion(userQuestion, key);
+                       if (handled) {
+                               if (userQuestion.getAnswer() != null) {
+                                       handleAction(userQuestion.getAction(),
+                                                       userQuestion.getAnswer());
+
+                                       userQuestion = null;
+                               }
+                       }
+               } else {
+                       handled = handleKey(key);
+               }
+
+               if (!handled) {
+                       handled = super.handleInput(key);
+               }
+
+               return handled;
+       }
+
+       /**
+        * Actually set the title <b>inside</b> the window. Will also call
+        * {@link BasicWindow#setTitle} with the computed parameters.
+        */
+       private void setTitle() {
                String prefix = " " + Main.APPLICATION_TITLE + " (version "
                                + Main.APPLICATION_VERSION + ")";
 
+               String title = null;
                int count = -1;
+
                MainContent content = getContent();
-               if (content != null)
+               if (content != null) {
+                       title = content.getTitle();
                        count = content.getCount();
+               }
+
+               if (title == null)
+                       title = "";
 
-               if (title != null) {
+               if (title.length() > 0) {
                        prefix = prefix + ": ";
+                       title = StringUtils.sanitize(title, Main.isUnicode());
                }
 
+               String countStr = "";
+               if (count > -1) {
+                       countStr = "[" + count + "]";
+               }
+
+               int width = -1;
                if (getSize() != null) {
-                       if (title != null)
-                               title = StringUtils.padString(title, getSize().getColumns());
-                       else
-                               // cause busy-loop freeze:
-                               prefix = StringUtils.padString(prefix, getSize().getColumns());
+                       width = getSize().getColumns();
+               }
+
+               if (width > 0) {
+                       int padding = width - prefix.length() - title.length()
+                                       - countStr.length();
+                       if (padding > 0) {
+                               if (title.length() > 0)
+                                       title = StringUtils.padString(title, title.length()
+                                                       + padding);
+                               else
+                                       prefix = StringUtils.padString(prefix, prefix.length()
+                                                       + padding);
+                       }
                }
-               
-               if (!(title + count).equals(this.title)) {
-                       this.title = title + count;
 
-                       super.setTitle(prefix + title);
+               String titleCache = prefix + title + count;
+               if (!titleCache.equals(this.titleCache)) {
+                       super.setTitle(prefix);
 
                        Label lblPrefix = new Label(prefix);
                        UiColors.Element.TITLE_MAIN.themeLabel(lblPrefix);
 
                        Label lblTitle = null;
-                       if (title != null) {
+                       if (title.length() > 0) {
                                lblTitle = new Label(title);
                                UiColors.Element.TITLE_VARIABLE.themeLabel(lblTitle);
                        }
 
                        Label lblCount = null;
-                       if (count > -1) {
-                               lblCount = new Label("[" + count + "]");
+                       if (countStr != null) {
+                               lblCount = new Label(countStr);
                                UiColors.Element.TITLE_COUNT.themeLabel(lblCount);
                        }
 
@@ -288,8 +461,6 @@ public class MainWindow extends BasicWindow {
                                titlePanel.addComponent(lblTitle, BorderLayout.Location.CENTER);
                        if (lblCount != null)
                                titlePanel.addComponent(lblCount, BorderLayout.Location.RIGHT);
-
-                       invalidate();
                }
        }
 
@@ -317,7 +488,6 @@ public class MainWindow extends BasicWindow {
        private void setActions(List<KeyAction> actions,
                        boolean enableDefaultactions) {
                this.actions.clear();
-               actionsPadded = false;
 
                if (enableDefaultactions)
                        this.actions.addAll(defaultActions);
@@ -327,34 +497,12 @@ public class MainWindow extends BasicWindow {
 
                actionPanel.removeAllComponents();
                for (KeyAction action : this.actions) {
-                       String trans = " " + action.getStringId().trans() + " ";
+                       String trans = " " + Main.trans(action.getStringId()) + " ";
 
                        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 = KeyAction.trans(action.getKey());
 
                        Panel kPane = new Panel();
                        LinearLayout layout = new LinearLayout(Direction.HORIZONTAL);
@@ -367,41 +515,77 @@ public class MainWindow extends BasicWindow {
 
                        actionPanel.addComponent(kPane);
                }
+
+               // 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)));
+               }
        }
 
        /**
         * Handle user input when in "ask for question" mode (see
-        * {@link MainWindow#questionKey}).
+        * {@link MainWindow#userQuestion}).
         * 
+        * @param userQuestion
+        *            the question data
         * @param key
         *            the key that has been pressed by the user
         * 
-        * @return the user's answer if done
+        * @return TRUE if the {@link KeyStroke} was handled
         */
-       private String handleQuestion(KeyStroke key) {
-               String answer = null;
+       private boolean handleQuestion(UserQuestion userQuestion, KeyStroke key) {
+               userQuestion.setAnswer(null);
 
-               if (waitForOneKeyAnswer) {
-                       answer = "" + key.getCharacter();
+               if (userQuestion.isOneKeyAnswer()) {
+                       userQuestion.setAnswer("" + key.getCharacter());
                } else {
-                       if (key.getKeyType() == KeyType.Enter) {
+                       // ^h == Backspace
+                       if (key.isCtrlDown() && key.getCharacter() == 'h') {
+                               key = new KeyStroke(KeyType.Backspace);
+                       }
+
+                       switch (key.getKeyType()) {
+                       case Enter:
                                if (text != null)
-                                       answer = text.getText();
+                                       userQuestion.setAnswer(text.getText());
                                else
-                                       answer = "";
+                                       userQuestion.setAnswer("");
+                               break;
+                       case Backspace:
+                               int pos = text.getCaretPosition().getColumn();
+                               if (pos > 0) {
+                                       String current = text.getText();
+                                       // force caret one space before:
+                                       text.setText(current.substring(0, pos - 1));
+                                       // re-add full text:
+                                       text.setText(current.substring(0, pos - 1)
+                                                       + current.substring(pos));
+                               }
+                               return true;
+                       default:
+                               // Do nothing (continue entering text)
+                               break;
                        }
                }
 
-               if (answer != null) {
+               if (userQuestion.getAnswer() != null) {
                        Interactable focus = null;
                        MainContent content = getContent();
                        if (content != null)
                                focus = content.nextFocus(null);
 
                        focus.takeFocus();
+
+                       return true;
                }
 
-               return answer;
+               return false;
        }
 
        /**
@@ -412,90 +596,22 @@ public class MainWindow extends BasicWindow {
         * @param answer
         *            the answer given for this key
         * 
-        * @return if the window handled the inout
+        * @return if the window handled the input
         */
-       private boolean handleInput(KeyStroke key, String answer) {
+       private boolean handleKey(KeyStroke key) {
                boolean handled = false;
 
-               setMessage(null, false);
+               if (setMessage(null, false))
+                       return true;
 
                for (KeyAction action : actions) {
                        if (!action.match(key))
                                continue;
 
-                       MainContent content = getContent();
                        handled = true;
 
                        if (action.onAction()) {
-                               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:
-                                       Card card = action.getCard();
-                                       if (card != null) {
-                                               pushContent(new ContactList(card));
-                                       }
-                                       break;
-                               case CONTACT_DETAILS:
-                                       Contact contact = action.getContact();
-                                       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?", false);
-                                       } else {
-                                               setMessage("You answered: " + answer, false);
-                                       }
-
-                                       handled = true;
-                                       break;
-                               case BACK:
-                                       if (content != null) {
-                                               String warning = content.getExitWarning();
-                                               if (warning != null) {
-                                                       if (answer == null) {
-                                                               setQuestion(key, warning, true);
-                                                       } else {
-                                                               if (answer.equalsIgnoreCase("y")) {
-                                                                       popContent();
-                                                               }
-                                                       }
-                                               } else {
-                                                       popContent();
-                                               }
-                                       }
-
-                                       if (contentStack.size() == 0)
-                                               close();
-                                       break;
-                               default:
-                               case NONE:
-                                       break;
-                               }
+                               handleAction(action, null);
                        }
 
                        break;
@@ -504,26 +620,107 @@ public class MainWindow extends BasicWindow {
                return handled;
        }
 
-       @Override
-       public boolean handleInput(KeyStroke key) {
-               boolean handled = false;
+       /**
+        * 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();
 
-               if (questionKey != null) {
-                       String answer = handleQuestion(key);
-                       if (answer != null) {
-                               // TODO
-                               key = questionKey;
-                               questionKey = null;
+               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);
+                       }
 
-                               handled = handleInput(key, answer);
+                       break;
+               // mode with windows:
+               case CONTACT_LIST:
+                       if (action.getCard() != null) {
+                               pushContent(new ContactList(action.getCard()));
                        }
-               } else {
-                       handled = handleInput(key, null);
-               }
+                       break;
+               case CONTACT_DETAILS:
+                       if (action.getContact() != null) {
+                               pushContent(new ContactDetails(action.getContact()));
+                       }
+                       break;
+               case CONTACT_DETAILS_RAW:
+                       if (action.getContact() != null) {
+                               pushContent(new ContactDetailsRaw(action.getContact()));
+                       }
+                       break;
+               // mode interpreted by MainWindow:
+               case HELP:
+                       // TODO
+                       setMessage("Help! I need somebody! Help!", false);
 
-               if (!handled)
-                       handled = super.handleInput(key);
+                       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();
+                       }
 
-               return handled;
+                       if (contentStack.size() == 0) {
+                               close();
+                       }
+
+                       break;
+               // action modes:
+               case ASK_USER:
+                       if (answer == null) {
+                               setQuestion(action, action.getQuestion(),
+                                               action.getDefaultAnswer());
+                       } else {
+                               setMessage(action.callback(answer), true);
+                               content.refreshData();
+                               invalidate();
+                               setTitle();
+                       }
+                       break;
+               case ASK_USER_KEY:
+                       if (answer == null) {
+                               setQuestion(action, action.getQuestion());
+                       } else {
+                               setMessage(action.callback(answer), true);
+                               content.refreshData();
+                               invalidate();
+                               setTitle();
+                       }
+                       break;
+               default:
+               case NONE:
+                       break;
+               }
        }
 }