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