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