a09751ef26a81dbbfa3f00c1e869fd9757ca05d9
[jvcard.git] / src / be / nikiroo / jvcard / tui / MainWindow.java
1 package be.nikiroo.jvcard.tui;
2
3 import java.util.Arrays;
4 import java.util.LinkedList;
5 import java.util.List;
6
7 import be.nikiroo.jvcard.Card;
8 import be.nikiroo.jvcard.Contact;
9 import be.nikiroo.jvcard.i18n.Trans.StringId;
10 import be.nikiroo.jvcard.tui.KeyAction.Mode;
11 import be.nikiroo.jvcard.tui.UiColors.Element;
12 import be.nikiroo.jvcard.tui.panes.ContactDetails;
13 import be.nikiroo.jvcard.tui.panes.ContactList;
14 import be.nikiroo.jvcard.tui.panes.MainContent;
15
16 import com.googlecode.lanterna.TerminalSize;
17 import com.googlecode.lanterna.TextColor;
18 import com.googlecode.lanterna.gui2.BasicWindow;
19 import com.googlecode.lanterna.gui2.BorderLayout;
20 import com.googlecode.lanterna.gui2.ComponentRenderer;
21 import com.googlecode.lanterna.gui2.Direction;
22 import com.googlecode.lanterna.gui2.Interactable;
23 import com.googlecode.lanterna.gui2.Label;
24 import com.googlecode.lanterna.gui2.LinearLayout;
25 import com.googlecode.lanterna.gui2.Panel;
26 import com.googlecode.lanterna.gui2.TextBox;
27 import com.googlecode.lanterna.gui2.TextGUIGraphics;
28 import com.googlecode.lanterna.gui2.Window;
29 import com.googlecode.lanterna.input.KeyStroke;
30 import com.googlecode.lanterna.input.KeyType;
31
32 /**
33 * This is the main "window" of the program. It can show up to one
34 * {@link MainContent} at any one time, but keeps a stack of contents.
35 *
36 * @author niki
37 *
38 */
39 public class MainWindow extends BasicWindow {
40 private List<KeyAction> defaultActions = new LinkedList<KeyAction>();
41 private List<KeyAction> actions = new LinkedList<KeyAction>();
42 private List<MainContent> contentStack = new LinkedList<MainContent>();
43 private boolean actionsPadded;
44 private boolean waitForOneKeyAnswer;
45 private KeyStroke questionKey; // key that "asked" a question, and to replay
46 // later with an answer
47 private String title;
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 String title = null;
134
135 contentPanel.removeAllComponents();
136 if (content != null) {
137 title = content.getTitle();
138 actions = content.getKeyBindings();
139 contentPanel.addComponent(content, BorderLayout.Location.CENTER);
140 this.contentStack.add(content);
141
142 Interactable focus = content.nextFocus(null);
143 if (focus != null)
144 focus.takeFocus();
145 }
146
147 setTitle(title);
148 setActions(actions, true);
149
150 invalidate();
151 }
152
153 /**
154 * "pop" the latest, top-of-the-stack content from the window stack.
155 *
156 * @return the removed content if any
157 */
158 public MainContent popContent() {
159 MainContent removed = null;
160 MainContent prev = null;
161
162 MainContent content = getContent();
163 if (content != null)
164 removed = contentStack.remove(contentStack.size() - 1);
165
166 if (contentStack.size() > 0)
167 prev = contentStack.remove(contentStack.size() - 1);
168
169 pushContent(prev);
170
171 return removed;
172 }
173
174 /**
175 * Show the given message on screen. It will disappear at the next action.
176 *
177 * @param mess
178 * the message to display
179 * @param error
180 * TRUE for an error message, FALSE for an information message
181 */
182 public void setMessage(String mess, boolean error) {
183 messagePanel.removeAllComponents();
184 if (mess != null) {
185 Element element = (error ? UiColors.Element.LINE_MESSAGE_ERR
186 : UiColors.Element.LINE_MESSAGE);
187 Label lbl = element.createLabel(" " + mess + " ");
188 messagePanel.addComponent(lbl, LinearLayout
189 .createLayoutData(LinearLayout.Alignment.Center));
190 }
191 }
192
193 /**
194 * Show a question to the user and switch to "ask for answer" mode see
195 * {@link MainWindow#handleQuestion}.
196 *
197 * @param mess
198 * the message to display
199 * @param oneKey
200 * TRUE for a one-key answer, FALSE for a text answer validated
201 * by ENTER
202 */
203 public void setQuestion(KeyStroke key, String mess, boolean oneKey) {
204 questionKey = key;
205 waitForOneKeyAnswer = oneKey;
206
207 messagePanel.removeAllComponents();
208
209 Panel hpanel = new Panel();
210 LinearLayout llayout = new LinearLayout(Direction.HORIZONTAL);
211 llayout.setSpacing(0);
212 hpanel.setLayoutManager(llayout);
213
214 Label lbl = UiColors.Element.LINE_MESSAGE_QUESTION.createLabel(" "
215 + mess + " ");
216 text = new TextBox(new TerminalSize(getSize().getColumns()
217 - lbl.getSize().getColumns(), 1));
218
219 hpanel.addComponent(lbl, LinearLayout
220 .createLayoutData(LinearLayout.Alignment.Beginning));
221 hpanel.addComponent(text, LinearLayout
222 .createLayoutData(LinearLayout.Alignment.Fill));
223
224 messagePanel.addComponent(hpanel, LinearLayout
225 .createLayoutData(LinearLayout.Alignment.Beginning));
226
227 text.takeFocus();
228 }
229
230 @Override
231 public void draw(TextGUIGraphics graphics) {
232 if (!actionsPadded) {
233 // fill with "desc" colour
234 actionPanel.addComponent(UiColors.Element.ACTION_DESC
235 .createLabel(StringUtils.padString("", graphics.getSize()
236 .getColumns())));
237 actionsPadded = true;
238 }
239 super.draw(graphics);
240 }
241
242 @Override
243 public void setTitle(String title) {
244 String prefix = " " + Main.APPLICATION_TITLE + " (version "
245 + Main.APPLICATION_VERSION + ")";
246
247 int count = -1;
248 MainContent content = getContent();
249 if (content != null)
250 count = content.getCount();
251
252 if (title != null) {
253 prefix = prefix + ": ";
254 }
255
256 if (getSize() != null) {
257 if (title != null)
258 title = StringUtils.padString(title, getSize().getColumns());
259 else
260 // cause busy-loop freeze:
261 prefix = StringUtils.padString(prefix, getSize().getColumns());
262 }
263
264 if (!(title + count).equals(this.title)) {
265 this.title = title + count;
266
267 super.setTitle(prefix + title);
268
269 Label lblPrefix = new Label(prefix);
270 UiColors.Element.TITLE_MAIN.themeLabel(lblPrefix);
271
272 Label lblTitle = null;
273 if (title != null) {
274 lblTitle = new Label(title);
275 UiColors.Element.TITLE_VARIABLE.themeLabel(lblTitle);
276 }
277
278 Label lblCount = null;
279 if (count > -1) {
280 lblCount = new Label("[" + count + "]");
281 UiColors.Element.TITLE_COUNT.themeLabel(lblCount);
282 }
283
284 titlePanel.removeAllComponents();
285
286 titlePanel.addComponent(lblPrefix, BorderLayout.Location.LEFT);
287 if (lblTitle != null)
288 titlePanel.addComponent(lblTitle, BorderLayout.Location.CENTER);
289 if (lblCount != null)
290 titlePanel.addComponent(lblCount, BorderLayout.Location.RIGHT);
291
292 invalidate();
293 }
294 }
295
296 /**
297 * Return the current {@link MainContent} from the stack if any.
298 *
299 * @return the current {@link MainContent}
300 */
301 private MainContent getContent() {
302 if (contentStack.size() > 0) {
303 return contentStack.get(contentStack.size() - 1);
304 }
305
306 return null;
307 }
308
309 /**
310 * Update the list of actions and refresh the action panel.
311 *
312 * @param actions
313 * the list of actions to support
314 * @param enableDefaultactions
315 * TRUE to enable the default actions
316 */
317 private void setActions(List<KeyAction> actions,
318 boolean enableDefaultactions) {
319 this.actions.clear();
320 actionsPadded = false;
321
322 if (enableDefaultactions)
323 this.actions.addAll(defaultActions);
324
325 if (actions != null)
326 this.actions.addAll(actions);
327
328 actionPanel.removeAllComponents();
329 for (KeyAction action : this.actions) {
330 String trans = " " + action.getStringId().trans() + " ";
331
332 if (" ".equals(trans))
333 continue;
334
335 String keyTrans = "";
336 switch (action.getKey().getKeyType()) {
337 case Enter:
338 keyTrans = " ⤶ ";
339 break;
340 case Tab:
341 keyTrans = " ↹ ";
342 break;
343 case Character:
344 keyTrans = " " + action.getKey().getCharacter() + " ";
345 break;
346 default:
347 keyTrans = "" + action.getKey().getKeyType();
348 int width = 3;
349 if (keyTrans.length() > width) {
350 keyTrans = keyTrans.substring(0, width);
351 } else if (keyTrans.length() < width) {
352 keyTrans = keyTrans
353 + new String(new char[width - keyTrans.length()])
354 .replace('\0', ' ');
355 }
356 break;
357 }
358
359 Panel kPane = new Panel();
360 LinearLayout layout = new LinearLayout(Direction.HORIZONTAL);
361 layout.setSpacing(0);
362 kPane.setLayoutManager(layout);
363
364 kPane.addComponent(UiColors.Element.ACTION_KEY
365 .createLabel(keyTrans));
366 kPane.addComponent(UiColors.Element.ACTION_DESC.createLabel(trans));
367
368 actionPanel.addComponent(kPane);
369 }
370 }
371
372 /**
373 * Handle user input when in "ask for question" mode (see
374 * {@link MainWindow#questionKey}).
375 *
376 * @param key
377 * the key that has been pressed by the user
378 *
379 * @return the user's answer if done
380 */
381 private String handleQuestion(KeyStroke key) {
382 String answer = null;
383
384 if (waitForOneKeyAnswer) {
385 answer = "" + key.getCharacter();
386 } else {
387 if (key.getKeyType() == KeyType.Enter) {
388 if (text != null)
389 answer = text.getText();
390 else
391 answer = "";
392 }
393 }
394
395 if (answer != null) {
396 Interactable focus = null;
397 MainContent content = getContent();
398 if (content != null)
399 focus = content.nextFocus(null);
400
401 focus.takeFocus();
402 }
403
404 return answer;
405 }
406
407 /**
408 * Handle the input in case of "normal" (not "ask for answer") mode.
409 *
410 * @param key
411 * the key that was pressed
412 * @param answer
413 * the answer given for this key
414 *
415 * @return if the window handled the inout
416 */
417 private boolean handleInput(KeyStroke key, String answer) {
418 boolean handled = false;
419
420 setMessage(null, false);
421
422 for (KeyAction action : actions) {
423 if (!action.match(key))
424 continue;
425
426 MainContent content = getContent();
427 handled = true;
428
429 if (action.onAction()) {
430 switch (action.getMode()) {
431 case MOVE:
432 int x = 0;
433 int y = 0;
434
435 if (action.getKey().getKeyType() == KeyType.ArrowUp)
436 x = -1;
437 if (action.getKey().getKeyType() == KeyType.ArrowDown)
438 x = 1;
439 if (action.getKey().getKeyType() == KeyType.ArrowLeft)
440 y = -1;
441 if (action.getKey().getKeyType() == KeyType.ArrowRight)
442 y = 1;
443
444 if (content != null) {
445 String err = content.move(x, y);
446 if (err != null)
447 setMessage(err, true);
448 }
449
450 break;
451 // mode with windows:
452 case CONTACT_LIST:
453 Card card = action.getCard();
454 if (card != null) {
455 pushContent(new ContactList(card));
456 }
457 break;
458 case CONTACT_DETAILS:
459 Contact contact = action.getContact();
460 if (contact != null) {
461 pushContent(new ContactDetails(contact));
462 }
463 break;
464 // mode interpreted by MainWindow:
465 case HELP:
466 // TODO
467 // setMessage("Help! I need somebody! Help!", false);
468 if (answer == null) {
469 setQuestion(key, "Test question?", false);
470 } else {
471 setMessage("You answered: " + answer, false);
472 }
473
474 handled = true;
475 break;
476 case BACK:
477 if (content != null) {
478 String warning = content.getExitWarning();
479 if (warning != null) {
480 if (answer == null) {
481 setQuestion(key, warning, true);
482 } else {
483 if (answer.equalsIgnoreCase("y")) {
484 popContent();
485 }
486 }
487 } else {
488 popContent();
489 }
490 }
491
492 if (contentStack.size() == 0)
493 close();
494 break;
495 default:
496 case NONE:
497 break;
498 }
499 }
500
501 break;
502 }
503
504 return handled;
505 }
506
507 @Override
508 public boolean handleInput(KeyStroke key) {
509 boolean handled = false;
510
511 if (questionKey != null) {
512 String answer = handleQuestion(key);
513 if (answer != null) {
514 // TODO
515 key = questionKey;
516 questionKey = null;
517
518 handled = handleInput(key, answer);
519 }
520 } else {
521 handled = handleInput(key, null);
522 }
523
524 if (!handled)
525 handled = super.handleInput(key);
526
527 return handled;
528 }
529 }