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