Add text-image control and separate Edit/View contact
[jvcard.git] / src / be / nikiroo / jvcard / tui / MainWindow.java
CommitLineData
a3b510ab
NR
1package be.nikiroo.jvcard.tui;
2
bcb54330
NR
3import java.io.IOException;
4import java.util.ArrayList;
a3b510ab
NR
5import java.util.Arrays;
6import java.util.LinkedList;
7import java.util.List;
8
9import be.nikiroo.jvcard.Card;
10import be.nikiroo.jvcard.Contact;
bcb54330 11import be.nikiroo.jvcard.Data;
296a0b75 12import be.nikiroo.jvcard.i18n.Trans;
a3b510ab
NR
13import be.nikiroo.jvcard.i18n.Trans.StringId;
14import be.nikiroo.jvcard.tui.KeyAction.Mode;
15import be.nikiroo.jvcard.tui.UiColors.Element;
f04d8b1c 16import be.nikiroo.jvcard.tui.panes.ContactDetails;
296a0b75 17import be.nikiroo.jvcard.tui.panes.ContactDetailsRaw;
fae07ea7
NR
18import be.nikiroo.jvcard.tui.panes.ContactList;
19import be.nikiroo.jvcard.tui.panes.MainContent;
a3b510ab
NR
20
21import com.googlecode.lanterna.TerminalSize;
22import com.googlecode.lanterna.gui2.BasicWindow;
23import com.googlecode.lanterna.gui2.BorderLayout;
24import com.googlecode.lanterna.gui2.Direction;
25import com.googlecode.lanterna.gui2.Interactable;
26import com.googlecode.lanterna.gui2.Label;
27import com.googlecode.lanterna.gui2.LinearLayout;
28import com.googlecode.lanterna.gui2.Panel;
29import com.googlecode.lanterna.gui2.TextBox;
a3b510ab
NR
30import com.googlecode.lanterna.gui2.Window;
31import com.googlecode.lanterna.input.KeyStroke;
32import com.googlecode.lanterna.input.KeyType;
33
34/**
0b0b2b0f
NR
35 * This is the main "window" of the program. It can show up to one
36 * {@link MainContent} at any one time, but keeps a stack of contents.
a3b510ab
NR
37 *
38 * @author niki
39 *
40 */
41public class MainWindow extends BasicWindow {
42 private List<KeyAction> defaultActions = new LinkedList<KeyAction>();
43 private List<KeyAction> actions = new LinkedList<KeyAction>();
0b0b2b0f 44 private List<MainContent> contentStack = new LinkedList<MainContent>();
0b0b2b0f 45 private boolean waitForOneKeyAnswer;
296a0b75 46 private KeyAction questionAction;
bcb54330 47 private String titleCache;
a3b510ab
NR
48 private Panel titlePanel;
49 private Panel mainPanel;
50 private Panel contentPanel;
51 private Panel actionPanel;
52 private Panel messagePanel;
53 private TextBox text;
54
0b0b2b0f
NR
55 /**
56 * Create a new, empty window.
57 */
a3b510ab
NR
58 public MainWindow() {
59 this(null);
60 }
61
0b0b2b0f
NR
62 /**
63 * Create a new window hosting the given content.
64 *
65 * @param content
66 * the content to host
67 */
a3b510ab
NR
68 public MainWindow(MainContent content) {
69 super(content == null ? "" : content.getTitle());
70
71 setHints(Arrays.asList(Window.Hint.FULL_SCREEN,
72 Window.Hint.NO_DECORATIONS, Window.Hint.FIT_TERMINAL_WINDOW));
73
74 defaultActions.add(new KeyAction(Mode.BACK, 'q',
75 StringId.KEY_ACTION_BACK));
76 defaultActions.add(new KeyAction(Mode.BACK, KeyType.Escape,
77 StringId.NULL));
78 defaultActions.add(new KeyAction(Mode.HELP, 'h',
79 StringId.KEY_ACTION_HELP));
80 defaultActions.add(new KeyAction(Mode.HELP, KeyType.F1, StringId.NULL));
81
82 actionPanel = new Panel();
83 contentPanel = new Panel();
84 mainPanel = new Panel();
85 messagePanel = new Panel();
86 titlePanel = new Panel();
87
88 Panel actionMessagePanel = new Panel();
89
90 LinearLayout llayout = new LinearLayout(Direction.HORIZONTAL);
91 llayout.setSpacing(0);
92 actionPanel.setLayoutManager(llayout);
93
0b0b2b0f
NR
94 BorderLayout blayout = new BorderLayout();
95 titlePanel.setLayoutManager(blayout);
a3b510ab
NR
96
97 llayout = new LinearLayout(Direction.VERTICAL);
98 llayout.setSpacing(0);
99 messagePanel.setLayoutManager(llayout);
100
0b0b2b0f 101 blayout = new BorderLayout();
a3b510ab
NR
102 mainPanel.setLayoutManager(blayout);
103
104 blayout = new BorderLayout();
105 contentPanel.setLayoutManager(blayout);
106
107 blayout = new BorderLayout();
108 actionMessagePanel.setLayoutManager(blayout);
109
110 actionMessagePanel
111 .addComponent(messagePanel, BorderLayout.Location.TOP);
112 actionMessagePanel.addComponent(actionPanel,
113 BorderLayout.Location.CENTER);
114
115 mainPanel.addComponent(titlePanel, BorderLayout.Location.TOP);
116 mainPanel.addComponent(contentPanel, BorderLayout.Location.CENTER);
117 mainPanel
118 .addComponent(actionMessagePanel, BorderLayout.Location.BOTTOM);
119
120 pushContent(content);
121
122 setComponent(mainPanel);
123 }
124
0b0b2b0f
NR
125 /**
126 * "push" some content to the window stack.
127 *
128 * @param content
129 * the new top-of-the-stack content
130 */
a3b510ab
NR
131 public void pushContent(MainContent content) {
132 List<KeyAction> actions = null;
a3b510ab
NR
133
134 contentPanel.removeAllComponents();
135 if (content != null) {
a3b510ab
NR
136 actions = content.getKeyBindings();
137 contentPanel.addComponent(content, BorderLayout.Location.CENTER);
0b0b2b0f 138 this.contentStack.add(content);
9c8baf0c
NR
139
140 Interactable focus = content.nextFocus(null);
141 if (focus != null)
142 focus.takeFocus();
a3b510ab
NR
143 }
144
bcb54330 145 setTitle();
0b0b2b0f 146 setActions(actions, true);
a3b510ab
NR
147 }
148
0b0b2b0f
NR
149 /**
150 * "pop" the latest, top-of-the-stack content from the window stack.
151 *
152 * @return the removed content if any
153 */
9c8baf0c
NR
154 public MainContent popContent() {
155 MainContent removed = null;
156 MainContent prev = null;
9c8baf0c 157
0b0b2b0f
NR
158 MainContent content = getContent();
159 if (content != null)
160 removed = contentStack.remove(contentStack.size() - 1);
9c8baf0c 161
0b0b2b0f
NR
162 if (contentStack.size() > 0)
163 prev = contentStack.remove(contentStack.size() - 1);
a3b510ab 164
0b0b2b0f 165 pushContent(prev);
a3b510ab 166
0b0b2b0f 167 return removed;
a3b510ab
NR
168 }
169
9c8baf0c
NR
170 /**
171 * Show the given message on screen. It will disappear at the next action.
172 *
173 * @param mess
174 * the message to display
175 * @param error
176 * TRUE for an error message, FALSE for an information message
bcb54330
NR
177 *
178 * @return TRUE if changes were performed
9c8baf0c 179 */
bcb54330
NR
180 public boolean setMessage(String mess, boolean error) {
181 if (mess != null || messagePanel.getChildCount() > 0) {
182 messagePanel.removeAllComponents();
183 if (mess != null) {
184 Element element = (error ? UiColors.Element.LINE_MESSAGE_ERR
185 : UiColors.Element.LINE_MESSAGE);
186 Label lbl = element.createLabel(" " + mess + " ");
187 messagePanel.addComponent(lbl, LinearLayout
188 .createLayoutData(LinearLayout.Alignment.Center));
189 }
190 return true;
191 }
192
193 return false;
194 }
195
196 /**
197 * Show a question to the user and switch to "ask for answer" mode see
198 * {@link MainWindow#handleQuestion}. The user will be asked to enter some
199 * answer and confirm with ENTER.
200 *
296a0b75
NR
201 * @param action
202 * the related action
bcb54330
NR
203 * @param question
204 * the question to ask
205 * @param initial
206 * the initial answer if any (to be edited by the user)
207 */
296a0b75
NR
208 public void setQuestion(KeyAction action, String question, String initial) {
209 setQuestion(action, question, initial, false);
bcb54330
NR
210 }
211
212 /**
213 * Show a question to the user and switch to "ask for answer" mode see
214 * {@link MainWindow#handleQuestion}. The user will be asked to hit one key
215 * as an answer.
216 *
296a0b75
NR
217 * @param action
218 * the related action
bcb54330
NR
219 * @param question
220 * the question to ask
221 */
296a0b75
NR
222 public void setQuestion(KeyAction action, String question) {
223 setQuestion(action, question, null, true);
9c8baf0c
NR
224 }
225
0b0b2b0f
NR
226 /**
227 * Show a question to the user and switch to "ask for answer" mode see
228 * {@link MainWindow#handleQuestion}.
229 *
296a0b75
NR
230 * @param action
231 * the related action
bcb54330
NR
232 * @param question
233 * the question to ask
234 * @param initial
235 * the initial answer if any (to be edited by the user)
0b0b2b0f
NR
236 * @param oneKey
237 * TRUE for a one-key answer, FALSE for a text answer validated
238 * by ENTER
239 */
296a0b75 240 private void setQuestion(KeyAction action, String question, String initial,
bcb54330 241 boolean oneKey) {
296a0b75 242 questionAction = action;
0b0b2b0f
NR
243 waitForOneKeyAnswer = oneKey;
244
9c8baf0c 245 messagePanel.removeAllComponents();
9c8baf0c 246
0b0b2b0f
NR
247 Panel hpanel = new Panel();
248 LinearLayout llayout = new LinearLayout(Direction.HORIZONTAL);
249 llayout.setSpacing(0);
250 hpanel.setLayoutManager(llayout);
9c8baf0c 251
0b0b2b0f 252 Label lbl = UiColors.Element.LINE_MESSAGE_QUESTION.createLabel(" "
bcb54330 253 + question + " ");
296a0b75
NR
254 text = new TextBox(new TerminalSize(getSize().getColumns()
255 - lbl.getSize().getColumns(), 1));
bcb54330
NR
256 if (initial != null)
257 text.setText(initial);
9c8baf0c 258
bcb54330
NR
259 hpanel.addComponent(lbl,
260 LinearLayout.createLayoutData(LinearLayout.Alignment.Beginning));
261 hpanel.addComponent(text,
262 LinearLayout.createLayoutData(LinearLayout.Alignment.Fill));
9c8baf0c 263
bcb54330
NR
264 messagePanel
265 .addComponent(hpanel, LinearLayout
266 .createLayoutData(LinearLayout.Alignment.Beginning));
9c8baf0c 267
0b0b2b0f 268 text.takeFocus();
9c8baf0c
NR
269 }
270
296a0b75
NR
271 /**
272 * Refresh the window and the empty-space handling. You should call this
273 * method when the window size changed.
274 *
275 * @param size
276 * the new size of the window
277 */
278 public void refresh(TerminalSize size) {
279 if (size == null)
280 return;
281
282 if (getSize() == null || !getSize().equals(size))
283 setSize(size);
284
285 setTitle();
286
287 if (actions != null)
288 setActions(new ArrayList<KeyAction>(actions), false);
289
290 invalidate();
291 }
292
293 @Override
294 public void invalidate() {
295 super.invalidate();
296 for (MainContent content : contentStack) {
297 content.invalidate();
298 }
299 }
300
301 @Override
302 public boolean handleInput(KeyStroke key) {
303 boolean handled = false;
304
305 if (questionAction != null) {
306 String answer = handleQuestion(key);
307 if (answer != null) {
308 handled = true;
309
310 handleAction(questionAction, answer);
311 questionAction = null;
312 }
313 } else {
314 handled = handleKey(key);
315 }
316
317 if (!handled)
318 handled = super.handleInput(key);
319
320 return handled;
321 }
322
bcb54330
NR
323 /**
324 * Actually set the title <b>inside</b> the window. Will also call
296a0b75 325 * {@link BasicWindow#setTitle} with the computed parameters.
bcb54330
NR
326 */
327 private void setTitle() {
0b0b2b0f
NR
328 String prefix = " " + Main.APPLICATION_TITLE + " (version "
329 + Main.APPLICATION_VERSION + ")";
330
bcb54330 331 String title = null;
0b0b2b0f 332 int count = -1;
bcb54330 333
0b0b2b0f 334 MainContent content = getContent();
bcb54330
NR
335 if (content != null) {
336 title = content.getTitle();
0b0b2b0f 337 count = content.getCount();
bcb54330
NR
338 }
339
340 if (title == null)
341 title = "";
0b0b2b0f 342
bcb54330 343 if (title.length() > 0) {
0b0b2b0f 344 prefix = prefix + ": ";
296a0b75
NR
345 title = StringUtils.sanitize(title, UiColors.getInstance()
346 .isUnicode());
0b0b2b0f
NR
347 }
348
bcb54330
NR
349 String countStr = "";
350 if (count > -1) {
351 countStr = "[" + count + "]";
352 }
353
296a0b75
NR
354 int width = -1;
355 if (getSize() != null) {
356 width = getSize().getColumns();
357 }
358
bcb54330
NR
359 if (width > 0) {
360 int padding = width - prefix.length() - title.length()
361 - countStr.length();
362 if (padding > 0) {
363 if (title.length() > 0)
364 title = StringUtils.padString(title, title.length()
365 + padding);
366 else
367 prefix = StringUtils.padString(prefix, prefix.length()
368 + padding);
369 }
0b0b2b0f 370 }
0b0b2b0f 371
bcb54330
NR
372 String titleCache = prefix + title + count;
373 if (!titleCache.equals(this.titleCache)) {
374 super.setTitle(prefix);
0b0b2b0f
NR
375
376 Label lblPrefix = new Label(prefix);
377 UiColors.Element.TITLE_MAIN.themeLabel(lblPrefix);
378
379 Label lblTitle = null;
bcb54330 380 if (title.length() > 0) {
0b0b2b0f
NR
381 lblTitle = new Label(title);
382 UiColors.Element.TITLE_VARIABLE.themeLabel(lblTitle);
383 }
384
385 Label lblCount = null;
bcb54330
NR
386 if (countStr != null) {
387 lblCount = new Label(countStr);
0b0b2b0f
NR
388 UiColors.Element.TITLE_COUNT.themeLabel(lblCount);
389 }
390
391 titlePanel.removeAllComponents();
a3b510ab 392
0b0b2b0f
NR
393 titlePanel.addComponent(lblPrefix, BorderLayout.Location.LEFT);
394 if (lblTitle != null)
395 titlePanel.addComponent(lblTitle, BorderLayout.Location.CENTER);
396 if (lblCount != null)
397 titlePanel.addComponent(lblCount, BorderLayout.Location.RIGHT);
0b0b2b0f
NR
398 }
399 }
400
401 /**
402 * Return the current {@link MainContent} from the stack if any.
403 *
404 * @return the current {@link MainContent}
405 */
406 private MainContent getContent() {
407 if (contentStack.size() > 0) {
408 return contentStack.get(contentStack.size() - 1);
409 }
410
411 return null;
412 }
413
414 /**
415 * Update the list of actions and refresh the action panel.
416 *
417 * @param actions
418 * the list of actions to support
419 * @param enableDefaultactions
420 * TRUE to enable the default actions
421 */
422 private void setActions(List<KeyAction> actions,
423 boolean enableDefaultactions) {
a3b510ab 424 this.actions.clear();
9c8baf0c 425
a3b510ab
NR
426 if (enableDefaultactions)
427 this.actions.addAll(defaultActions);
428
429 if (actions != null)
430 this.actions.addAll(actions);
431
432 actionPanel.removeAllComponents();
433 for (KeyAction action : this.actions) {
434 String trans = " " + action.getStringId().trans() + " ";
435
436 if (" ".equals(trans))
437 continue;
438
296a0b75 439 String keyTrans = Trans.getInstance().trans(action.getKey());
a3b510ab
NR
440
441 Panel kPane = new Panel();
442 LinearLayout layout = new LinearLayout(Direction.HORIZONTAL);
443 layout.setSpacing(0);
444 kPane.setLayoutManager(layout);
445
446 kPane.addComponent(UiColors.Element.ACTION_KEY
447 .createLabel(keyTrans));
448 kPane.addComponent(UiColors.Element.ACTION_DESC.createLabel(trans));
449
450 actionPanel.addComponent(kPane);
451 }
bcb54330
NR
452
453 // fill with "desc" colour
296a0b75
NR
454 int width = -1;
455 if (getSize() != null) {
456 width = getSize().getColumns();
457 }
458
bcb54330
NR
459 if (width > 0) {
460 actionPanel.addComponent(UiColors.Element.ACTION_DESC
461 .createLabel(StringUtils.padString("", width)));
bcb54330 462 }
a3b510ab
NR
463 }
464
0b0b2b0f
NR
465 /**
466 * Handle user input when in "ask for question" mode (see
467 * {@link MainWindow#questionKey}).
468 *
469 * @param key
470 * the key that has been pressed by the user
471 *
472 * @return the user's answer if done
473 */
a3b510ab
NR
474 private String handleQuestion(KeyStroke key) {
475 String answer = null;
476
477 if (waitForOneKeyAnswer) {
478 answer = "" + key.getCharacter();
479 } else {
480 if (key.getKeyType() == KeyType.Enter) {
481 if (text != null)
482 answer = text.getText();
483 else
484 answer = "";
485 }
486 }
487
488 if (answer != null) {
489 Interactable focus = null;
0b0b2b0f
NR
490 MainContent content = getContent();
491 if (content != null)
492 focus = content.nextFocus(null);
a3b510ab 493
fae07ea7 494 focus.takeFocus();
a3b510ab
NR
495 }
496
497 return answer;
498 }
499
0b0b2b0f
NR
500 /**
501 * Handle the input in case of "normal" (not "ask for answer") mode.
502 *
503 * @param key
504 * the key that was pressed
505 * @param answer
506 * the answer given for this key
507 *
bcb54330 508 * @return if the window handled the input
0b0b2b0f 509 */
296a0b75 510 private boolean handleKey(KeyStroke key) {
a3b510ab
NR
511 boolean handled = false;
512
296a0b75
NR
513 if (setMessage(null, false))
514 return true;
a3b510ab 515
0b0b2b0f
NR
516 for (KeyAction action : actions) {
517 if (!action.match(key))
518 continue;
a3b510ab 519
0b0b2b0f
NR
520 handled = true;
521
522 if (action.onAction()) {
296a0b75
NR
523 handleAction(action, null);
524 }
0b0b2b0f 525
296a0b75
NR
526 break;
527 }
0b0b2b0f 528
296a0b75
NR
529 return handled;
530 }
531
532 /**
533 * Handle the input in case of "normal" (not "ask for answer") mode.
534 *
535 * @param key
536 * the key that was pressed
537 * @param answer
538 * the answer given for this key
539 *
540 * @return if the window handled the input
541 */
542 private void handleAction(KeyAction action, String answer) {
543 MainContent content = getContent();
544
545 Card card = action.getCard();
546 Contact contact = action.getContact();
547 Data data = action.getData();
548
549 switch (action.getMode()) {
550 case MOVE:
551 int x = 0;
552 int y = 0;
553
554 if (action.getKey().getKeyType() == KeyType.ArrowUp)
555 x = -1;
556 if (action.getKey().getKeyType() == KeyType.ArrowDown)
557 x = 1;
558 if (action.getKey().getKeyType() == KeyType.ArrowLeft)
559 y = -1;
560 if (action.getKey().getKeyType() == KeyType.ArrowRight)
561 y = 1;
562
563 if (content != null) {
564 String err = content.move(x, y);
565 if (err != null)
566 setMessage(err, true);
567 }
568
569 break;
570 // mode with windows:
571 case CONTACT_LIST:
572 if (card != null) {
573 pushContent(new ContactList(card));
574 }
575 break;
576 case CONTACT_DETAILS:
f04d8b1c
NR
577 if (contact != null) {
578 pushContent(new ContactDetails(contact));
579 }
580 break;
581 case CONTACT_DETAILS_RAW:
296a0b75
NR
582 if (contact != null) {
583 pushContent(new ContactDetailsRaw(contact));
584 }
585 break;
586 // mode interpreted by MainWindow:
587 case HELP:
588 // TODO
589 // setMessage("Help! I need somebody! Help!", false);
590 if (answer == null) {
591 setQuestion(action, "Test question?", "[initial]");
592 } else {
593 setMessage("You answered: " + answer, false);
594 }
595
596 break;
597 case BACK:
598 String warning = content.getExitWarning();
599 if (warning != null) {
600 if (answer == null) {
601 setQuestion(action, warning);
602 } else {
603 setMessage(null, false);
604 if (answer.equalsIgnoreCase("y")) {
bcb54330 605 popContent();
a3b510ab 606 }
296a0b75
NR
607 }
608 } else {
609 popContent();
610 }
0b0b2b0f 611
296a0b75
NR
612 if (contentStack.size() == 0) {
613 close();
614 }
bcb54330 615
296a0b75
NR
616 break;
617 // action modes:
618 case EDIT_DETAIL:
619 if (answer == null) {
620 if (data != null) {
621 String name = data.getName();
622 String value = data.getValue();
623 setQuestion(action, name, value);
624 }
625 } else {
626 setMessage(null, false);
627 data.setValue(answer);
628 }
629 break;
630 case DELETE_CONTACT:
631 if (answer == null) {
632 if (contact != null) {
633 setQuestion(action, "Delete contact? [Y/N]");
634 }
635 } else {
636 setMessage(null, false);
637 if (answer.equalsIgnoreCase("y")) {
638 if (contact.delete()) {
639 content.refreshData();
640 invalidate();
641 setTitle();
bcb54330 642 } else {
296a0b75 643 setMessage("Cannot delete this contact", true);
bcb54330 644 }
296a0b75
NR
645 }
646 }
647 break;
648 case SAVE_CARD:
649 if (answer == null) {
650 if (card != null) {
651 setQuestion(action, "Save changes? [Y/N]");
652 }
653 } else {
654 setMessage(null, false);
655 if (answer.equalsIgnoreCase("y")) {
656 boolean ok = false;
657 try {
658 if (card.save()) {
659 ok = true;
660 invalidate();
bcb54330 661 }
296a0b75
NR
662 } catch (IOException ioe) {
663 ioe.printStackTrace();
bcb54330 664 }
296a0b75
NR
665
666 if (!ok) {
667 setMessage("Cannot save to file", true);
bcb54330 668 }
a3b510ab 669 }
0b0b2b0f 670 }
296a0b75
NR
671 break;
672 default:
673 case NONE:
0b0b2b0f
NR
674 break;
675 }
a3b510ab
NR
676 }
677}