KeyAction management now more generic
[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;
296a0b75 12import be.nikiroo.jvcard.i18n.Trans;
a3b510ab
NR
13import be.nikiroo.jvcard.i18n.Trans.StringId;
14import be.nikiroo.jvcard.tui.KeyAction.Mode;
15import be.nikiroo.jvcard.tui.UiColors.Element;
f04d8b1c 16import be.nikiroo.jvcard.tui.panes.ContactDetails;
296a0b75 17import be.nikiroo.jvcard.tui.panes.ContactDetailsRaw;
fae07ea7
NR
18import be.nikiroo.jvcard.tui.panes.ContactList;
19import be.nikiroo.jvcard.tui.panes.MainContent;
a3b510ab
NR
20
21import com.googlecode.lanterna.TerminalSize;
22import com.googlecode.lanterna.gui2.BasicWindow;
23import com.googlecode.lanterna.gui2.BorderLayout;
24import com.googlecode.lanterna.gui2.Direction;
25import com.googlecode.lanterna.gui2.Interactable;
26import com.googlecode.lanterna.gui2.Label;
27import com.googlecode.lanterna.gui2.LinearLayout;
28import com.googlecode.lanterna.gui2.Panel;
29import com.googlecode.lanterna.gui2.TextBox;
a3b510ab
NR
30import com.googlecode.lanterna.gui2.Window;
31import com.googlecode.lanterna.input.KeyStroke;
32import com.googlecode.lanterna.input.KeyType;
33
34/**
0b0b2b0f
NR
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.
a3b510ab
NR
37 *
38 * @author niki
39 *
40 */
41public class MainWindow extends BasicWindow {
42 private List<KeyAction> defaultActions = new LinkedList<KeyAction>();
43 private List<KeyAction> actions = new LinkedList<KeyAction>();
0b0b2b0f 44 private List<MainContent> contentStack = new LinkedList<MainContent>();
0b0b2b0f 45 private boolean waitForOneKeyAnswer;
296a0b75 46 private KeyAction questionAction;
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;
54
0b0b2b0f
NR
55 /**
56 * Create a new, empty window.
57 */
a3b510ab
NR
58 public MainWindow() {
59 this(null);
60 }
61
0b0b2b0f
NR
62 /**
63 * Create a new window hosting the given content.
64 *
65 * @param content
66 * the content to host
67 */
a3b510ab
NR
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
0b0b2b0f
NR
94 BorderLayout blayout = new BorderLayout();
95 titlePanel.setLayoutManager(blayout);
a3b510ab
NR
96
97 llayout = new LinearLayout(Direction.VERTICAL);
98 llayout.setSpacing(0);
99 messagePanel.setLayoutManager(llayout);
100
0b0b2b0f 101 blayout = new BorderLayout();
a3b510ab
NR
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
0b0b2b0f
NR
125 /**
126 * "push" some content to the window stack.
127 *
128 * @param content
129 * the new top-of-the-stack content
130 */
a3b510ab
NR
131 public void pushContent(MainContent content) {
132 List<KeyAction> actions = null;
a3b510ab
NR
133
134 contentPanel.removeAllComponents();
135 if (content != null) {
a3b510ab
NR
136 actions = content.getKeyBindings();
137 contentPanel.addComponent(content, BorderLayout.Location.CENTER);
0b0b2b0f 138 this.contentStack.add(content);
9c8baf0c
NR
139
140 Interactable focus = content.nextFocus(null);
141 if (focus != null)
142 focus.takeFocus();
a3b510ab
NR
143 }
144
bcb54330 145 setTitle();
0b0b2b0f 146 setActions(actions, true);
a3b510ab
NR
147 }
148
0b0b2b0f
NR
149 /**
150 * "pop" the latest, top-of-the-stack content from the window stack.
151 *
152 * @return the removed content if any
153 */
9c8baf0c
NR
154 public MainContent popContent() {
155 MainContent removed = null;
156 MainContent prev = null;
9c8baf0c 157
0b0b2b0f
NR
158 MainContent content = getContent();
159 if (content != null)
160 removed = contentStack.remove(contentStack.size() - 1);
9c8baf0c 161
0b0b2b0f
NR
162 if (contentStack.size() > 0)
163 prev = contentStack.remove(contentStack.size() - 1);
a3b510ab 164
0b0b2b0f 165 pushContent(prev);
a3b510ab 166
0b0b2b0f 167 return removed;
a3b510ab
NR
168 }
169
9c8baf0c
NR
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
bcb54330
NR
177 *
178 * @return TRUE if changes were performed
9c8baf0c 179 */
bcb54330
NR
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 *
296a0b75
NR
201 * @param action
202 * the related action
bcb54330
NR
203 * @param question
204 * the question to ask
205 * @param initial
206 * the initial answer if any (to be edited by the user)
207 */
296a0b75
NR
208 public void setQuestion(KeyAction action, String question, String initial) {
209 setQuestion(action, question, initial, false);
bcb54330
NR
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 *
296a0b75
NR
217 * @param action
218 * the related action
bcb54330
NR
219 * @param question
220 * the question to ask
221 */
296a0b75
NR
222 public void setQuestion(KeyAction action, String question) {
223 setQuestion(action, question, null, true);
9c8baf0c
NR
224 }
225
0b0b2b0f
NR
226 /**
227 * Show a question to the user and switch to "ask for answer" mode see
228 * {@link MainWindow#handleQuestion}.
229 *
296a0b75
NR
230 * @param action
231 * the related action
bcb54330
NR
232 * @param question
233 * the question to ask
234 * @param initial
235 * the initial answer if any (to be edited by the user)
0b0b2b0f
NR
236 * @param oneKey
237 * TRUE for a one-key answer, FALSE for a text answer validated
238 * by ENTER
239 */
296a0b75 240 private void setQuestion(KeyAction action, String question, String initial,
bcb54330 241 boolean oneKey) {
296a0b75 242 questionAction = action;
0b0b2b0f
NR
243 waitForOneKeyAnswer = oneKey;
244
9c8baf0c 245 messagePanel.removeAllComponents();
9c8baf0c 246
0b0b2b0f
NR
247 Panel hpanel = new Panel();
248 LinearLayout llayout = new LinearLayout(Direction.HORIZONTAL);
249 llayout.setSpacing(0);
250 hpanel.setLayoutManager(llayout);
9c8baf0c 251
0b0b2b0f 252 Label lbl = UiColors.Element.LINE_MESSAGE_QUESTION.createLabel(" "
bcb54330 253 + question + " ");
296a0b75
NR
254 text = new TextBox(new TerminalSize(getSize().getColumns()
255 - lbl.getSize().getColumns(), 1));
bcb54330
NR
256 if (initial != null)
257 text.setText(initial);
9c8baf0c 258
bcb54330
NR
259 hpanel.addComponent(lbl,
260 LinearLayout.createLayoutData(LinearLayout.Alignment.Beginning));
261 hpanel.addComponent(text,
262 LinearLayout.createLayoutData(LinearLayout.Alignment.Fill));
9c8baf0c 263
bcb54330
NR
264 messagePanel
265 .addComponent(hpanel, LinearLayout
266 .createLayoutData(LinearLayout.Alignment.Beginning));
9c8baf0c 267
0b0b2b0f 268 text.takeFocus();
9c8baf0c
NR
269 }
270
296a0b75
NR
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
bcb54330
NR
323 /**
324 * Actually set the title <b>inside</b> the window. Will also call
296a0b75 325 * {@link BasicWindow#setTitle} with the computed parameters.
bcb54330
NR
326 */
327 private void setTitle() {
0b0b2b0f
NR
328 String prefix = " " + Main.APPLICATION_TITLE + " (version "
329 + Main.APPLICATION_VERSION + ")";
330
bcb54330 331 String title = null;
0b0b2b0f 332 int count = -1;
bcb54330 333
0b0b2b0f 334 MainContent content = getContent();
bcb54330
NR
335 if (content != null) {
336 title = content.getTitle();
0b0b2b0f 337 count = content.getCount();
bcb54330
NR
338 }
339
340 if (title == null)
341 title = "";
0b0b2b0f 342
bcb54330 343 if (title.length() > 0) {
0b0b2b0f 344 prefix = prefix + ": ";
296a0b75
NR
345 title = StringUtils.sanitize(title, UiColors.getInstance()
346 .isUnicode());
0b0b2b0f
NR
347 }
348
bcb54330
NR
349 String countStr = "";
350 if (count > -1) {
351 countStr = "[" + count + "]";
352 }
353
296a0b75
NR
354 int width = -1;
355 if (getSize() != null) {
356 width = getSize().getColumns();
357 }
358
bcb54330
NR
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 }
0b0b2b0f 370 }
0b0b2b0f 371
bcb54330
NR
372 String titleCache = prefix + title + count;
373 if (!titleCache.equals(this.titleCache)) {
374 super.setTitle(prefix);
0b0b2b0f
NR
375
376 Label lblPrefix = new Label(prefix);
377 UiColors.Element.TITLE_MAIN.themeLabel(lblPrefix);
378
379 Label lblTitle = null;
bcb54330 380 if (title.length() > 0) {
0b0b2b0f
NR
381 lblTitle = new Label(title);
382 UiColors.Element.TITLE_VARIABLE.themeLabel(lblTitle);
383 }
384
385 Label lblCount = null;
bcb54330
NR
386 if (countStr != null) {
387 lblCount = new Label(countStr);
0b0b2b0f
NR
388 UiColors.Element.TITLE_COUNT.themeLabel(lblCount);
389 }
390
391 titlePanel.removeAllComponents();
a3b510ab 392
0b0b2b0f
NR
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);
0b0b2b0f
NR
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) {
a3b510ab 424 this.actions.clear();
9c8baf0c 425
a3b510ab
NR
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
296a0b75 439 String keyTrans = Trans.getInstance().trans(action.getKey());
a3b510ab
NR
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 }
bcb54330
NR
452
453 // fill with "desc" colour
296a0b75
NR
454 int width = -1;
455 if (getSize() != null) {
456 width = getSize().getColumns();
457 }
458
bcb54330
NR
459 if (width > 0) {
460 actionPanel.addComponent(UiColors.Element.ACTION_DESC
461 .createLabel(StringUtils.padString("", width)));
bcb54330 462 }
a3b510ab
NR
463 }
464
0b0b2b0f
NR
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 */
a3b510ab
NR
474 private String handleQuestion(KeyStroke key) {
475 String answer = null;
476
ae22c247
NR
477 // TODO: support ^H (backspace)
478 // TODO: start at end of initial question, not start
479
a3b510ab
NR
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;
0b0b2b0f
NR
493 MainContent content = getContent();
494 if (content != null)
495 focus = content.nextFocus(null);
a3b510ab 496
fae07ea7 497 focus.takeFocus();
a3b510ab
NR
498 }
499
500 return answer;
501 }
502
0b0b2b0f
NR
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 *
bcb54330 511 * @return if the window handled the input
0b0b2b0f 512 */
296a0b75 513 private boolean handleKey(KeyStroke key) {
a3b510ab
NR
514 boolean handled = false;
515
296a0b75
NR
516 if (setMessage(null, false))
517 return true;
a3b510ab 518
0b0b2b0f
NR
519 for (KeyAction action : actions) {
520 if (!action.match(key))
521 continue;
a3b510ab 522
0b0b2b0f
NR
523 handled = true;
524
525 if (action.onAction()) {
296a0b75
NR
526 handleAction(action, null);
527 }
0b0b2b0f 528
296a0b75
NR
529 break;
530 }
0b0b2b0f 531
296a0b75
NR
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
296a0b75
NR
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:
ae22c247
NR
571 if (action.getCard() != null) {
572 pushContent(new ContactList(action.getCard()));
296a0b75
NR
573 }
574 break;
575 case CONTACT_DETAILS:
ae22c247
NR
576 if (action.getContact() != null) {
577 pushContent(new ContactDetails(action.getContact()));
f04d8b1c
NR
578 }
579 break;
580 case CONTACT_DETAILS_RAW:
ae22c247
NR
581 if (action.getContact() != null) {
582 pushContent(new ContactDetailsRaw(action.getContact()));
296a0b75
NR
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")) {
bcb54330 604 popContent();
a3b510ab 605 }
296a0b75
NR
606 }
607 } else {
608 popContent();
609 }
0b0b2b0f 610
296a0b75
NR
611 if (contentStack.size() == 0) {
612 close();
613 }
bcb54330 614
296a0b75
NR
615 break;
616 // action modes:
ae22c247 617 case ASK_USER:
296a0b75 618 if (answer == null) {
ae22c247
NR
619 setQuestion(action, action.getQuestion(),
620 action.getDefaultAnswer());
296a0b75 621 } else {
ae22c247
NR
622 setMessage(action.callback(answer), true);
623 content.refreshData();
624 invalidate();
625 setTitle();
296a0b75
NR
626 }
627 break;
ae22c247 628 case ASK_USER_KEY:
296a0b75 629 if (answer == null) {
ae22c247 630 setQuestion(action, action.getQuestion());
296a0b75 631 } else {
ae22c247
NR
632 setMessage(action.callback(answer), true);
633 content.refreshData();
634 invalidate();
635 setTitle();
0b0b2b0f 636 }
296a0b75
NR
637 break;
638 default:
639 case NONE:
0b0b2b0f
NR
640 break;
641 }
a3b510ab
NR
642 }
643}