Improve UI, take "dirty" check into account, move launcher to Main.java
[jvcard.git] / src / be / nikiroo / jvcard / tui / MainWindow.java
index aa996353c9b2b8d292dfa92761f763c94bd98a09..a09751ef26a81dbbfa3f00c1e869fd9757ca05d9 100644 (file)
@@ -1,13 +1,11 @@
 package be.nikiroo.jvcard.tui;
 
-import java.io.File;
 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;
 import be.nikiroo.jvcard.i18n.Trans.StringId;
 import be.nikiroo.jvcard.tui.KeyAction.Mode;
 import be.nikiroo.jvcard.tui.UiColors.Element;
@@ -16,8 +14,10 @@ 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;
@@ -30,8 +30,8 @@ import com.googlecode.lanterna.input.KeyStroke;
 import com.googlecode.lanterna.input.KeyType;
 
 /**
- * This is the main "window" of the program. It will host one
- * {@link MainContent} at any one time.
+ * This is the main "window" of the program. It can show up to one
+ * {@link MainContent} at any one time, but keeps a stack of contents.
  * 
  * @author niki
  * 
@@ -39,10 +39,11 @@ import com.googlecode.lanterna.input.KeyType;
 public class MainWindow extends BasicWindow {
        private List<KeyAction> defaultActions = new LinkedList<KeyAction>();
        private List<KeyAction> actions = new LinkedList<KeyAction>();
-       private List<MainContent> content = new LinkedList<MainContent>();
+       private List<MainContent> contentStack = new LinkedList<MainContent>();
        private boolean actionsPadded;
-       private Boolean waitForOneKeyAnswer; // true, false, (null = do not wait for
-       // an answer)
+       private boolean waitForOneKeyAnswer;
+       private KeyStroke questionKey; // key that "asked" a question, and to replay
+       // later with an answer
        private String title;
        private Panel titlePanel;
        private Panel mainPanel;
@@ -51,10 +52,19 @@ public class MainWindow extends BasicWindow {
        private Panel messagePanel;
        private TextBox text;
 
+       /**
+        * Create a new, empty window.
+        */
        public MainWindow() {
                this(null);
        }
 
+       /**
+        * Create a new window hosting the given content.
+        * 
+        * @param content
+        *            the content to host
+        */
        public MainWindow(MainContent content) {
                super(content == null ? "" : content.getTitle());
 
@@ -81,15 +91,14 @@ public class MainWindow extends BasicWindow {
                llayout.setSpacing(0);
                actionPanel.setLayoutManager(llayout);
 
-               llayout = new LinearLayout(Direction.VERTICAL);
-               llayout.setSpacing(0);
-               titlePanel.setLayoutManager(llayout);
+               BorderLayout blayout = new BorderLayout();
+               titlePanel.setLayoutManager(blayout);
 
                llayout = new LinearLayout(Direction.VERTICAL);
                llayout.setSpacing(0);
                messagePanel.setLayoutManager(llayout);
 
-               BorderLayout blayout = new BorderLayout();
+               blayout = new BorderLayout();
                mainPanel.setLayoutManager(blayout);
 
                blayout = new BorderLayout();
@@ -113,6 +122,12 @@ public class MainWindow extends BasicWindow {
                setComponent(mainPanel);
        }
 
+       /**
+        * "push" some content to the window stack.
+        * 
+        * @param content
+        *            the new top-of-the-stack content
+        */
        public void pushContent(MainContent content) {
                List<KeyAction> actions = null;
                String title = null;
@@ -122,7 +137,7 @@ public class MainWindow extends BasicWindow {
                        title = content.getTitle();
                        actions = content.getKeyBindings();
                        contentPanel.addComponent(content, BorderLayout.Location.CENTER);
-                       this.content.add(content);
+                       this.contentStack.add(content);
 
                        Interactable focus = content.nextFocus(null);
                        if (focus != null)
@@ -130,44 +145,30 @@ public class MainWindow extends BasicWindow {
                }
 
                setTitle(title);
-               setActions(actions, true, true);
+               setActions(actions, true);
 
                invalidate();
        }
 
+       /**
+        * "pop" the latest, top-of-the-stack content from the window stack.
+        * 
+        * @return the removed content if any
+        */
        public MainContent popContent() {
                MainContent removed = null;
                MainContent prev = null;
-               if (content.size() > 0)
-                       removed = content.remove(content.size() - 1);
-               if (content.size() > 0)
-                       prev = content.remove(content.size() - 1);
-               pushContent(prev);
 
-               return removed;
-       }
+               MainContent content = getContent();
+               if (content != null)
+                       removed = contentStack.remove(contentStack.size() - 1);
 
-       /**
-        * Set the application title.
-        * 
-        * @param title
-        *            the new title or NULL for the default title
-        */
-       public void setTitle(String title) {
-               if (title == null) {
-                       title = Trans.StringId.TITLE.trans();
-               }
+               if (contentStack.size() > 0)
+                       prev = contentStack.remove(contentStack.size() - 1);
 
-               if (!title.equals(this.title)) {
-                       super.setTitle(title);
-                       this.title = title;
-               }
-
-               Label lbl = new Label(title);
-               titlePanel.removeAllComponents();
+               pushContent(prev);
 
-               titlePanel.addComponent(lbl, LinearLayout
-                               .createLayoutData(LinearLayout.Alignment.Center));
+               return removed;
        }
 
        /**
@@ -189,36 +190,45 @@ public class MainWindow extends BasicWindow {
                }
        }
 
-       public void setQuestion(String mess, boolean oneKey) {
+       /**
+        * Show a question to the user and switch to "ask for answer" mode see
+        * {@link MainWindow#handleQuestion}.
+        * 
+        * @param mess
+        *            the message to display
+        * @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;
+
                messagePanel.removeAllComponents();
-               if (mess != null) {
-                       waitForOneKeyAnswer = oneKey;
 
-                       Panel hpanel = new Panel();
-                       LinearLayout llayout = new LinearLayout(Direction.HORIZONTAL);
-                       llayout.setSpacing(0);
-                       hpanel.setLayoutManager(llayout);
+               Panel hpanel = new Panel();
+               LinearLayout llayout = new LinearLayout(Direction.HORIZONTAL);
+               llayout.setSpacing(0);
+               hpanel.setLayoutManager(llayout);
 
-                       Label lbl = UiColors.Element.LINE_MESSAGE_QUESTION.createLabel(" "
-                                       + mess + " ");
-                       text = new TextBox(new TerminalSize(getSize().getColumns()
-                                       - lbl.getSize().getColumns(), 1));
+               Label lbl = UiColors.Element.LINE_MESSAGE_QUESTION.createLabel(" "
+                               + mess + " ");
+               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));
+               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();
-               }
+               text.takeFocus();
        }
 
        @Override
        public void draw(TextGUIGraphics graphics) {
-               setTitle(title);
                if (!actionsPadded) {
                        // fill with "desc" colour
                        actionPanel.addComponent(UiColors.Element.ACTION_DESC
@@ -229,9 +239,83 @@ public class MainWindow extends BasicWindow {
                super.draw(graphics);
        }
 
-       private void setActions(List<KeyAction> actions, boolean allowKeys,
-                       boolean enableDefaultactions) {
+       @Override
+       public void setTitle(String title) {
+               String prefix = " " + Main.APPLICATION_TITLE + " (version "
+                               + Main.APPLICATION_VERSION + ")";
+
+               int count = -1;
+               MainContent content = getContent();
+               if (content != null)
+                       count = content.getCount();
+
+               if (title != null) {
+                       prefix = prefix + ": ";
+               }
+
+               if (getSize() != null) {
+                       if (title != null)
+                               title = StringUtils.padString(title, getSize().getColumns());
+                       else
+                               // cause busy-loop freeze:
+                               prefix = StringUtils.padString(prefix, getSize().getColumns());
+               }
+               
+               if (!(title + count).equals(this.title)) {
+                       this.title = title + count;
+
+                       super.setTitle(prefix + title);
+
+                       Label lblPrefix = new Label(prefix);
+                       UiColors.Element.TITLE_MAIN.themeLabel(lblPrefix);
+
+                       Label lblTitle = null;
+                       if (title != null) {
+                               lblTitle = new Label(title);
+                               UiColors.Element.TITLE_VARIABLE.themeLabel(lblTitle);
+                       }
+
+                       Label lblCount = null;
+                       if (count > -1) {
+                               lblCount = new Label("[" + count + "]");
+                               UiColors.Element.TITLE_COUNT.themeLabel(lblCount);
+                       }
+
+                       titlePanel.removeAllComponents();
 
+                       titlePanel.addComponent(lblPrefix, BorderLayout.Location.LEFT);
+                       if (lblTitle != null)
+                               titlePanel.addComponent(lblTitle, BorderLayout.Location.CENTER);
+                       if (lblCount != null)
+                               titlePanel.addComponent(lblCount, BorderLayout.Location.RIGHT);
+
+                       invalidate();
+               }
+       }
+
+       /**
+        * Return the current {@link MainContent} from the stack if any.
+        * 
+        * @return the current {@link MainContent}
+        */
+       private MainContent getContent() {
+               if (contentStack.size() > 0) {
+                       return contentStack.get(contentStack.size() - 1);
+               }
+
+               return null;
+       }
+
+       /**
+        * Update the list of actions and refresh the action panel.
+        * 
+        * @param actions
+        *            the list of actions to support
+        * @param enableDefaultactions
+        *            TRUE to enable the default actions
+        */
+       private void setActions(List<KeyAction> actions,
+                       boolean enableDefaultactions) {
                this.actions.clear();
                actionsPadded = false;
 
@@ -285,6 +369,15 @@ public class MainWindow extends BasicWindow {
                }
        }
 
+       /**
+        * Handle user input when in "ask for question" mode (see
+        * {@link MainWindow#questionKey}).
+        * 
+        * @param key
+        *            the key that has been pressed by the user
+        * 
+        * @return the user's answer if done
+        */
        private String handleQuestion(KeyStroke key) {
                String answer = null;
 
@@ -301,9 +394,9 @@ public class MainWindow extends BasicWindow {
 
                if (answer != null) {
                        Interactable focus = null;
-                       if (this.content.size() > 0)
-                               // focus = content.get(0).getDefaultFocusElement();
-                               focus = content.get(0).nextFocus(null);
+                       MainContent content = getContent();
+                       if (content != null)
+                               focus = content.nextFocus(null);
 
                        focus.takeFocus();
                }
@@ -311,83 +404,121 @@ public class MainWindow extends BasicWindow {
                return answer;
        }
 
-       @Override
-       public boolean handleInput(KeyStroke key) {
+       /**
+        * 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 inout
+        */
+       private boolean handleInput(KeyStroke key, String answer) {
                boolean handled = false;
 
-               if (waitForOneKeyAnswer != null) {
-                       String answer = handleQuestion(key);
-                       if (answer != null) {
-                               waitForOneKeyAnswer = null;
-                               setMessage("ANS: " + answer, false);
+               setMessage(null, false);
 
-                               handled = true;
-                       }
-               } else {
-                       setMessage(null, false);
-
-                       for (KeyAction action : actions) {
-                               if (!action.match(key))
-                                       continue;
-
-                               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.size() > 0) {
-                                                       String err = content.get(content.size() - 1).move(
-                                                                       x, y);
-                                                       if (err != null)
-                                                               setMessage(err, true);
-                                               }
+               for (KeyAction action : actions) {
+                       if (!action.match(key))
+                               continue;
 
-                                               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));
+                       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();
                                                }
-                                               break;
-                                       // mode interpreted by MainWindow:
-                                       case HELP:
-                                               // TODO
-                                               // setMessage("Help! I need somebody! Help!", false);
-                                               setQuestion("Test question?", false);
-                                               handled = true;
-                                               break;
-                                       case BACK:
-                                               popContent();
-                                               if (content.size() == 0)
-                                                       close();
-                                               break;
-                                       default:
-                                       case NONE:
-                                               break;
                                        }
+
+                                       if (contentStack.size() == 0)
+                                               close();
+                                       break;
+                               default:
+                               case NONE:
+                                       break;
                                }
+                       }
 
-                               break;
+                       break;
+               }
+
+               return handled;
+       }
+
+       @Override
+       public boolean handleInput(KeyStroke key) {
+               boolean handled = false;
+
+               if (questionKey != null) {
+                       String answer = handleQuestion(key);
+                       if (answer != null) {
+                               // TODO
+                               key = questionKey;
+                               questionKey = null;
+
+                               handled = handleInput(key, answer);
                        }
+               } else {
+                       handled = handleInput(key, null);
                }
 
                if (!handled)