Add text-image control and separate Edit/View contact
[jvcard.git] / src / be / nikiroo / jvcard / tui / MainWindow.java
1 package be.nikiroo.jvcard.tui;
2
3 import java.io.IOException;
4 import java.util.ArrayList;
5 import java.util.Arrays;
6 import java.util.LinkedList;
7 import java.util.List;
8
9 import be.nikiroo.jvcard.Card;
10 import be.nikiroo.jvcard.Contact;
11 import be.nikiroo.jvcard.Data;
12 import be.nikiroo.jvcard.i18n.Trans;
13 import be.nikiroo.jvcard.i18n.Trans.StringId;
14 import be.nikiroo.jvcard.tui.KeyAction.Mode;
15 import be.nikiroo.jvcard.tui.UiColors.Element;
16 import be.nikiroo.jvcard.tui.panes.ContactDetails;
17 import be.nikiroo.jvcard.tui.panes.ContactDetailsRaw;
18 import be.nikiroo.jvcard.tui.panes.ContactList;
19 import be.nikiroo.jvcard.tui.panes.MainContent;
20
21 import com.googlecode.lanterna.TerminalSize;
22 import com.googlecode.lanterna.gui2.BasicWindow;
23 import com.googlecode.lanterna.gui2.BorderLayout;
24 import com.googlecode.lanterna.gui2.Direction;
25 import com.googlecode.lanterna.gui2.Interactable;
26 import com.googlecode.lanterna.gui2.Label;
27 import com.googlecode.lanterna.gui2.LinearLayout;
28 import com.googlecode.lanterna.gui2.Panel;
29 import com.googlecode.lanterna.gui2.TextBox;
30 import com.googlecode.lanterna.gui2.Window;
31 import com.googlecode.lanterna.input.KeyStroke;
32 import com.googlecode.lanterna.input.KeyType;
33
34 /**
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.
37 *
38 * @author niki
39 *
40 */
41 public class MainWindow extends BasicWindow {
42 private List<KeyAction> defaultActions = new LinkedList<KeyAction>();
43 private List<KeyAction> actions = new LinkedList<KeyAction>();
44 private List<MainContent> contentStack = new LinkedList<MainContent>();
45 private boolean waitForOneKeyAnswer;
46 private KeyAction questionAction;
47 private String titleCache;
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
55 /**
56 * Create a new, empty window.
57 */
58 public MainWindow() {
59 this(null);
60 }
61
62 /**
63 * Create a new window hosting the given content.
64 *
65 * @param content
66 * the content to host
67 */
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
94 BorderLayout blayout = new BorderLayout();
95 titlePanel.setLayoutManager(blayout);
96
97 llayout = new LinearLayout(Direction.VERTICAL);
98 llayout.setSpacing(0);
99 messagePanel.setLayoutManager(llayout);
100
101 blayout = new BorderLayout();
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
125 /**
126 * "push" some content to the window stack.
127 *
128 * @param content
129 * the new top-of-the-stack content
130 */
131 public void pushContent(MainContent content) {
132 List<KeyAction> actions = null;
133
134 contentPanel.removeAllComponents();
135 if (content != null) {
136 actions = content.getKeyBindings();
137 contentPanel.addComponent(content, BorderLayout.Location.CENTER);
138 this.contentStack.add(content);
139
140 Interactable focus = content.nextFocus(null);
141 if (focus != null)
142 focus.takeFocus();
143 }
144
145 setTitle();
146 setActions(actions, true);
147 }
148
149 /**
150 * "pop" the latest, top-of-the-stack content from the window stack.
151 *
152 * @return the removed content if any
153 */
154 public MainContent popContent() {
155 MainContent removed = null;
156 MainContent prev = null;
157
158 MainContent content = getContent();
159 if (content != null)
160 removed = contentStack.remove(contentStack.size() - 1);
161
162 if (contentStack.size() > 0)
163 prev = contentStack.remove(contentStack.size() - 1);
164
165 pushContent(prev);
166
167 return removed;
168 }
169
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
177 *
178 * @return TRUE if changes were performed
179 */
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 *
201 * @param action
202 * the related action
203 * @param question
204 * the question to ask
205 * @param initial
206 * the initial answer if any (to be edited by the user)
207 */
208 public void setQuestion(KeyAction action, String question, String initial) {
209 setQuestion(action, question, initial, false);
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 *
217 * @param action
218 * the related action
219 * @param question
220 * the question to ask
221 */
222 public void setQuestion(KeyAction action, String question) {
223 setQuestion(action, question, null, true);
224 }
225
226 /**
227 * Show a question to the user and switch to "ask for answer" mode see
228 * {@link MainWindow#handleQuestion}.
229 *
230 * @param action
231 * the related action
232 * @param question
233 * the question to ask
234 * @param initial
235 * the initial answer if any (to be edited by the user)
236 * @param oneKey
237 * TRUE for a one-key answer, FALSE for a text answer validated
238 * by ENTER
239 */
240 private void setQuestion(KeyAction action, String question, String initial,
241 boolean oneKey) {
242 questionAction = action;
243 waitForOneKeyAnswer = oneKey;
244
245 messagePanel.removeAllComponents();
246
247 Panel hpanel = new Panel();
248 LinearLayout llayout = new LinearLayout(Direction.HORIZONTAL);
249 llayout.setSpacing(0);
250 hpanel.setLayoutManager(llayout);
251
252 Label lbl = UiColors.Element.LINE_MESSAGE_QUESTION.createLabel(" "
253 + question + " ");
254 text = new TextBox(new TerminalSize(getSize().getColumns()
255 - lbl.getSize().getColumns(), 1));
256 if (initial != null)
257 text.setText(initial);
258
259 hpanel.addComponent(lbl,
260 LinearLayout.createLayoutData(LinearLayout.Alignment.Beginning));
261 hpanel.addComponent(text,
262 LinearLayout.createLayoutData(LinearLayout.Alignment.Fill));
263
264 messagePanel
265 .addComponent(hpanel, LinearLayout
266 .createLayoutData(LinearLayout.Alignment.Beginning));
267
268 text.takeFocus();
269 }
270
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
323 /**
324 * Actually set the title <b>inside</b> the window. Will also call
325 * {@link BasicWindow#setTitle} with the computed parameters.
326 */
327 private void setTitle() {
328 String prefix = " " + Main.APPLICATION_TITLE + " (version "
329 + Main.APPLICATION_VERSION + ")";
330
331 String title = null;
332 int count = -1;
333
334 MainContent content = getContent();
335 if (content != null) {
336 title = content.getTitle();
337 count = content.getCount();
338 }
339
340 if (title == null)
341 title = "";
342
343 if (title.length() > 0) {
344 prefix = prefix + ": ";
345 title = StringUtils.sanitize(title, UiColors.getInstance()
346 .isUnicode());
347 }
348
349 String countStr = "";
350 if (count > -1) {
351 countStr = "[" + count + "]";
352 }
353
354 int width = -1;
355 if (getSize() != null) {
356 width = getSize().getColumns();
357 }
358
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 }
370 }
371
372 String titleCache = prefix + title + count;
373 if (!titleCache.equals(this.titleCache)) {
374 super.setTitle(prefix);
375
376 Label lblPrefix = new Label(prefix);
377 UiColors.Element.TITLE_MAIN.themeLabel(lblPrefix);
378
379 Label lblTitle = null;
380 if (title.length() > 0) {
381 lblTitle = new Label(title);
382 UiColors.Element.TITLE_VARIABLE.themeLabel(lblTitle);
383 }
384
385 Label lblCount = null;
386 if (countStr != null) {
387 lblCount = new Label(countStr);
388 UiColors.Element.TITLE_COUNT.themeLabel(lblCount);
389 }
390
391 titlePanel.removeAllComponents();
392
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);
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) {
424 this.actions.clear();
425
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
439 String keyTrans = Trans.getInstance().trans(action.getKey());
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 }
452
453 // fill with "desc" colour
454 int width = -1;
455 if (getSize() != null) {
456 width = getSize().getColumns();
457 }
458
459 if (width > 0) {
460 actionPanel.addComponent(UiColors.Element.ACTION_DESC
461 .createLabel(StringUtils.padString("", width)));
462 }
463 }
464
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 */
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;
490 MainContent content = getContent();
491 if (content != null)
492 focus = content.nextFocus(null);
493
494 focus.takeFocus();
495 }
496
497 return answer;
498 }
499
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 *
508 * @return if the window handled the input
509 */
510 private boolean handleKey(KeyStroke key) {
511 boolean handled = false;
512
513 if (setMessage(null, false))
514 return true;
515
516 for (KeyAction action : actions) {
517 if (!action.match(key))
518 continue;
519
520 handled = true;
521
522 if (action.onAction()) {
523 handleAction(action, null);
524 }
525
526 break;
527 }
528
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:
577 if (contact != null) {
578 pushContent(new ContactDetails(contact));
579 }
580 break;
581 case CONTACT_DETAILS_RAW:
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")) {
605 popContent();
606 }
607 }
608 } else {
609 popContent();
610 }
611
612 if (contentStack.size() == 0) {
613 close();
614 }
615
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();
642 } else {
643 setMessage("Cannot delete this contact", true);
644 }
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();
661 }
662 } catch (IOException ioe) {
663 ioe.printStackTrace();
664 }
665
666 if (!ok) {
667 setMessage("Cannot save to file", true);
668 }
669 }
670 }
671 break;
672 default:
673 case NONE:
674 break;
675 }
676 }
677 }