24e97e841f35e54db30bdda139e03cd56b1855ad
[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 // TODO: support ^H (backspace)
478 // TODO: start at end of initial question, not start
479
480 if (waitForOneKeyAnswer) {
481 answer = "" + key.getCharacter();
482 } else {
483 if (key.getKeyType() == KeyType.Enter) {
484 if (text != null)
485 answer = text.getText();
486 else
487 answer = "";
488 }
489 }
490
491 if (answer != null) {
492 Interactable focus = null;
493 MainContent content = getContent();
494 if (content != null)
495 focus = content.nextFocus(null);
496
497 focus.takeFocus();
498 }
499
500 return answer;
501 }
502
503 /**
504 * Handle the input in case of "normal" (not "ask for answer") mode.
505 *
506 * @param key
507 * the key that was pressed
508 * @param answer
509 * the answer given for this key
510 *
511 * @return if the window handled the input
512 */
513 private boolean handleKey(KeyStroke key) {
514 boolean handled = false;
515
516 if (setMessage(null, false))
517 return true;
518
519 for (KeyAction action : actions) {
520 if (!action.match(key))
521 continue;
522
523 handled = true;
524
525 if (action.onAction()) {
526 handleAction(action, null);
527 }
528
529 break;
530 }
531
532 return handled;
533 }
534
535 /**
536 * Handle the input in case of "normal" (not "ask for answer") mode.
537 *
538 * @param key
539 * the key that was pressed
540 * @param answer
541 * the answer given for this key
542 *
543 * @return if the window handled the input
544 */
545 private void handleAction(KeyAction action, String answer) {
546 MainContent content = getContent();
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 (action.getCard() != null) {
572 pushContent(new ContactList(action.getCard()));
573 }
574 break;
575 case CONTACT_DETAILS:
576 if (action.getContact() != null) {
577 pushContent(new ContactDetails(action.getContact()));
578 }
579 break;
580 case CONTACT_DETAILS_RAW:
581 if (action.getContact() != null) {
582 pushContent(new ContactDetailsRaw(action.getContact()));
583 }
584 break;
585 // mode interpreted by MainWindow:
586 case HELP:
587 // TODO
588 // setMessage("Help! I need somebody! Help!", false);
589 if (answer == null) {
590 setQuestion(action, "Test question?", "[initial]");
591 } else {
592 setMessage("You answered: " + answer, false);
593 }
594
595 break;
596 case BACK:
597 String warning = content.getExitWarning();
598 if (warning != null) {
599 if (answer == null) {
600 setQuestion(action, warning);
601 } else {
602 setMessage(null, false);
603 if (answer.equalsIgnoreCase("y")) {
604 popContent();
605 }
606 }
607 } else {
608 popContent();
609 }
610
611 if (contentStack.size() == 0) {
612 close();
613 }
614
615 break;
616 // action modes:
617 case ASK_USER:
618 if (answer == null) {
619 setQuestion(action, action.getQuestion(),
620 action.getDefaultAnswer());
621 } else {
622 setMessage(action.callback(answer), true);
623 content.refreshData();
624 invalidate();
625 setTitle();
626 }
627 break;
628 case ASK_USER_KEY:
629 if (answer == null) {
630 setQuestion(action, action.getQuestion());
631 } else {
632 setMessage(action.callback(answer), true);
633 content.refreshData();
634 invalidate();
635 setTitle();
636 }
637 break;
638 default:
639 case NONE:
640 break;
641 }
642 }
643 }