1 package be
.nikiroo
.jvcard
.tui
;
3 import java
.io
.IOException
;
4 import java
.util
.ArrayList
;
5 import java
.util
.Arrays
;
6 import java
.util
.LinkedList
;
9 import be
.nikiroo
.jvcard
.launcher
.Main
;
10 import be
.nikiroo
.jvcard
.resources
.StringUtils
;
11 import be
.nikiroo
.jvcard
.resources
.enums
.ColorOption
;
12 import be
.nikiroo
.jvcard
.resources
.enums
.StringId
;
13 import be
.nikiroo
.jvcard
.tui
.KeyAction
.Mode
;
14 import be
.nikiroo
.jvcard
.tui
.panes
.ContactDetails
;
15 import be
.nikiroo
.jvcard
.tui
.panes
.ContactDetailsRaw
;
16 import be
.nikiroo
.jvcard
.tui
.panes
.ContactList
;
17 import be
.nikiroo
.jvcard
.tui
.panes
.MainContent
;
19 import com
.googlecode
.lanterna
.TerminalSize
;
20 import com
.googlecode
.lanterna
.gui2
.BasicWindow
;
21 import com
.googlecode
.lanterna
.gui2
.BorderLayout
;
22 import com
.googlecode
.lanterna
.gui2
.Direction
;
23 import com
.googlecode
.lanterna
.gui2
.Interactable
;
24 import com
.googlecode
.lanterna
.gui2
.Label
;
25 import com
.googlecode
.lanterna
.gui2
.LinearLayout
;
26 import com
.googlecode
.lanterna
.gui2
.Panel
;
27 import com
.googlecode
.lanterna
.gui2
.TextBox
;
28 import com
.googlecode
.lanterna
.gui2
.Window
;
29 import com
.googlecode
.lanterna
.input
.KeyStroke
;
30 import com
.googlecode
.lanterna
.input
.KeyType
;
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.
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 UserQuestion userQuestion
;
45 private String titleCache
;
46 private Panel titlePanel
;
47 private Panel mainPanel
;
48 private Panel contentPanel
;
49 private Panel actionPanel
;
50 private Panel messagePanel
;
54 * Information about a question to ask the user and its answer.
59 private class UserQuestion
{
60 private boolean oneKeyAnswer
;
61 private KeyAction action
;
62 private String answer
;
65 * Create a new {@link UserQuestion}.
68 * the action that triggered the question
70 * TRUE if we expect a one-key answer
72 public UserQuestion(KeyAction action
, boolean oneKeyAnswer
) {
74 this.oneKeyAnswer
= oneKeyAnswer
;
78 * Return the {@link KeyAction} that triggered the question.
80 * @return the {@link KeyAction}
82 public KeyAction
getAction() {
87 * Check if a one-key answer is expected.
89 * @return TRUE if a one-key answer is expected
91 public boolean isOneKeyAnswer() {
96 * Return the user answer.
98 * @return the user answer
100 public String
getAnswer() {
105 * Set the user answer.
110 public void setAnswer(String answer
) {
111 this.answer
= answer
;
116 * Create a new, empty window.
118 public MainWindow() {
123 * Create a new window hosting the given content.
126 * the content to host
128 public MainWindow(MainContent content
) {
129 super(content
== null ?
"" : content
.getTitle());
131 setHints(Arrays
.asList(Window
.Hint
.FULL_SCREEN
,
132 Window
.Hint
.NO_DECORATIONS
, Window
.Hint
.FIT_TERMINAL_WINDOW
));
134 defaultActions
.add(new KeyAction(Mode
.BACK
, 'q',
135 StringId
.KEY_ACTION_BACK
));
136 defaultActions
.add(new KeyAction(Mode
.BACK
, KeyType
.Escape
,
138 defaultActions
.add(new KeyAction(Mode
.HELP
, 'h',
139 StringId
.KEY_ACTION_HELP
));
140 defaultActions
.add(new KeyAction(Mode
.HELP
, KeyType
.F1
, StringId
.NULL
));
142 actionPanel
= new Panel();
143 contentPanel
= new Panel();
144 mainPanel
= new Panel();
145 messagePanel
= new Panel();
146 titlePanel
= new Panel();
148 Panel actionMessagePanel
= new Panel();
150 LinearLayout llayout
= new LinearLayout(Direction
.HORIZONTAL
);
151 llayout
.setSpacing(0);
152 actionPanel
.setLayoutManager(llayout
);
154 BorderLayout blayout
= new BorderLayout();
155 titlePanel
.setLayoutManager(blayout
);
157 llayout
= new LinearLayout(Direction
.VERTICAL
);
158 llayout
.setSpacing(0);
159 messagePanel
.setLayoutManager(llayout
);
161 blayout
= new BorderLayout();
162 mainPanel
.setLayoutManager(blayout
);
164 blayout
= new BorderLayout();
165 contentPanel
.setLayoutManager(blayout
);
167 blayout
= new BorderLayout();
168 actionMessagePanel
.setLayoutManager(blayout
);
171 .addComponent(messagePanel
, BorderLayout
.Location
.TOP
);
172 actionMessagePanel
.addComponent(actionPanel
,
173 BorderLayout
.Location
.CENTER
);
175 mainPanel
.addComponent(titlePanel
, BorderLayout
.Location
.TOP
);
176 mainPanel
.addComponent(contentPanel
, BorderLayout
.Location
.CENTER
);
178 .addComponent(actionMessagePanel
, BorderLayout
.Location
.BOTTOM
);
180 pushContent(content
);
182 setComponent(mainPanel
);
186 * "push" some content to the window stack.
189 * the new top-of-the-stack content
191 public void pushContent(MainContent content
) {
192 List
<KeyAction
> actions
= null;
194 contentPanel
.removeAllComponents();
195 if (content
!= null) {
196 actions
= content
.getKeyBindings();
197 contentPanel
.addComponent(content
, BorderLayout
.Location
.CENTER
);
198 this.contentStack
.add(content
);
200 Interactable focus
= content
.nextFocus(null);
206 setActions(actions
, true);
210 * "pop" the latest, top-of-the-stack content from the window stack.
212 * @return the removed content if any
214 public MainContent
popContent() {
215 MainContent removed
= null;
216 MainContent prev
= null;
218 MainContent content
= getContent();
220 removed
= contentStack
.remove(contentStack
.size() - 1);
222 if (contentStack
.size() > 0)
223 prev
= contentStack
.remove(contentStack
.size() - 1);
227 String mess
= prev
.wakeup();
229 setMessage(mess
, false);
230 } catch (IOException e
) {
231 setMessage(e
.getMessage(), true);
241 * Show the given message on screen. It will disappear at the next action.
244 * the message to display
246 * TRUE for an error message, FALSE for an information message
248 * @return TRUE if changes were performed
250 public boolean setMessage(String mess
, boolean error
) {
251 if (mess
!= null || messagePanel
.getChildCount() > 0) {
252 messagePanel
.removeAllComponents();
254 ColorOption element
= (error ? ColorOption
.LINE_MESSAGE_ERR
255 : ColorOption
.LINE_MESSAGE
);
256 Label lbl
= UiColors
.createLabel(element
, " " + mess
+ " ");
257 messagePanel
.addComponent(lbl
, LinearLayout
258 .createLayoutData(LinearLayout
.Alignment
.Center
));
267 * Show a question to the user and switch to "ask for answer" mode see
268 * {@link MainWindow#handleQuestion}. The user will be asked to enter some
269 * answer and confirm with ENTER.
274 * the question to ask
276 * the initial answer if any (to be edited by the user)
278 public void setQuestion(KeyAction action
, String question
, String initial
) {
279 setQuestion(action
, question
, initial
, false);
283 * Show a question to the user and switch to "ask for answer" mode see
284 * {@link MainWindow#handleQuestion}. The user will be asked to hit one key
290 * the question to ask
292 public void setQuestion(KeyAction action
, String question
) {
293 setQuestion(action
, question
, null, true);
297 * Show a question to the user and switch to "ask for answer" mode see
298 * {@link MainWindow#handleQuestion}.
303 * the question to ask
305 * the initial answer if any (to be edited by the user)
307 * TRUE for a one-key answer, FALSE for a text answer validated
310 private void setQuestion(KeyAction action
, String question
, String initial
,
312 userQuestion
= new UserQuestion(action
, oneKey
);
314 messagePanel
.removeAllComponents();
316 Panel hpanel
= new Panel();
317 LinearLayout llayout
= new LinearLayout(Direction
.HORIZONTAL
);
318 llayout
.setSpacing(0);
319 hpanel
.setLayoutManager(llayout
);
321 Label lbl
= UiColors
.createLabel(ColorOption
.LINE_MESSAGE_QUESTION
, " "
323 text
= new TextBox(new TerminalSize(getSize().getColumns()
324 - lbl
.getSize().getColumns(), 1));
326 if (initial
!= null) {
327 // add all chars one by one so the caret is at the end
328 for (int index
= 0; index
< initial
.length(); index
++) {
329 text
.handleInput(new KeyStroke(initial
.charAt(index
), false,
334 hpanel
.addComponent(lbl
,
335 LinearLayout
.createLayoutData(LinearLayout
.Alignment
.Beginning
));
336 hpanel
.addComponent(text
,
337 LinearLayout
.createLayoutData(LinearLayout
.Alignment
.Fill
));
340 .addComponent(hpanel
, LinearLayout
341 .createLayoutData(LinearLayout
.Alignment
.Beginning
));
347 * Refresh the window and the empty-space handling. You should call this
348 * method when the window size changed.
351 * the new size of the window
353 public void refresh(TerminalSize size
) {
357 if (getSize() == null || !getSize().equals(size
))
363 setActions(new ArrayList
<KeyAction
>(actions
), false);
369 public void invalidate() {
371 for (MainContent content
: contentStack
) {
372 content
.invalidate();
377 public boolean handleInput(KeyStroke key
) {
378 boolean handled
= false;
380 if (userQuestion
!= null) {
381 handled
= handleQuestion(userQuestion
, key
);
383 if (userQuestion
.getAnswer() != null) {
384 handleAction(userQuestion
.getAction(),
385 userQuestion
.getAnswer());
391 handled
= handleKey(key
);
395 handled
= super.handleInput(key
);
402 * Actually set the title <b>inside</b> the window. Will also call
403 * {@link BasicWindow#setTitle} with the computed parameters.
405 private void setTitle() {
406 String prefix
= " " + Main
.APPLICATION_TITLE
+ " (version "
407 + Main
.APPLICATION_VERSION
+ ")";
412 MainContent content
= getContent();
413 if (content
!= null) {
414 title
= content
.getTitle();
415 count
= content
.getCount();
421 if (title
.length() > 0) {
422 prefix
= prefix
+ ": ";
423 title
= StringUtils
.sanitize(title
, Main
.isUnicode());
426 String countStr
= "";
428 countStr
= "[" + count
+ "]";
432 if (getSize() != null) {
433 width
= getSize().getColumns();
437 int padding
= width
- prefix
.length() - title
.length()
440 if (title
.length() > 0)
441 title
= StringUtils
.padString(title
, title
.length()
444 prefix
= StringUtils
.padString(prefix
, prefix
.length()
449 String titleCache
= prefix
+ title
+ count
;
450 if (!titleCache
.equals(this.titleCache
)) {
451 super.setTitle(prefix
);
453 Label lblPrefix
= new Label(prefix
);
454 UiColors
.themeLabel(ColorOption
.TITLE_MAIN
, lblPrefix
);
456 Label lblTitle
= null;
457 if (title
.length() > 0) {
458 lblTitle
= new Label(title
);
459 UiColors
.themeLabel(ColorOption
.TITLE_VARIABLE
, lblTitle
);
462 Label lblCount
= null;
463 if (countStr
!= null) {
464 lblCount
= new Label(countStr
);
465 UiColors
.themeLabel(ColorOption
.TITLE_COUNT
, lblCount
);
468 titlePanel
.removeAllComponents();
470 titlePanel
.addComponent(lblPrefix
, BorderLayout
.Location
.LEFT
);
471 if (lblTitle
!= null)
472 titlePanel
.addComponent(lblTitle
, BorderLayout
.Location
.CENTER
);
473 if (lblCount
!= null)
474 titlePanel
.addComponent(lblCount
, BorderLayout
.Location
.RIGHT
);
479 * Return the current {@link MainContent} from the stack if any.
481 * @return the current {@link MainContent}
483 private MainContent
getContent() {
484 if (contentStack
.size() > 0) {
485 return contentStack
.get(contentStack
.size() - 1);
492 * Update the list of actions and refresh the action panel.
495 * the list of actions to support
496 * @param enableDefaultactions
497 * TRUE to enable the default actions
499 private void setActions(List
<KeyAction
> actions
,
500 boolean enableDefaultactions
) {
501 this.actions
.clear();
503 if (enableDefaultactions
)
504 this.actions
.addAll(defaultActions
);
507 this.actions
.addAll(actions
);
509 actionPanel
.removeAllComponents();
510 for (KeyAction action
: this.actions
) {
511 String trans
= " " + Main
.trans(action
.getStringId()) + " ";
513 if (" ".equals(trans
))
516 String keyTrans
= KeyAction
.trans(action
.getKey());
518 Panel kPane
= new Panel();
519 LinearLayout layout
= new LinearLayout(Direction
.HORIZONTAL
);
520 layout
.setSpacing(0);
521 kPane
.setLayoutManager(layout
);
523 kPane
.addComponent(UiColors
.createLabel(ColorOption
.ACTION_KEY
,
525 kPane
.addComponent(UiColors
.createLabel(ColorOption
.ACTION_DESC
,
528 actionPanel
.addComponent(kPane
);
531 // fill with "desc" colour
533 if (getSize() != null) {
534 width
= getSize().getColumns();
538 actionPanel
.addComponent(UiColors
.createLabel(
539 ColorOption
.ACTION_DESC
, StringUtils
.padString("", width
)));
544 * Handle user input when in "ask for question" mode (see
545 * {@link MainWindow#userQuestion}).
547 * @param userQuestion
550 * the key that has been pressed by the user
552 * @return TRUE if the {@link KeyStroke} was handled
554 private boolean handleQuestion(UserQuestion userQuestion
, KeyStroke key
) {
555 userQuestion
.setAnswer(null);
557 if (userQuestion
.isOneKeyAnswer()) {
558 userQuestion
.setAnswer("" + key
.getCharacter());
561 if (key
.isCtrlDown() && key
.getCharacter() == 'h') {
562 key
= new KeyStroke(KeyType
.Backspace
);
565 switch (key
.getKeyType()) {
568 userQuestion
.setAnswer(text
.getText());
570 userQuestion
.setAnswer("");
573 int pos
= text
.getCaretPosition().getColumn();
575 String current
= text
.getText();
576 // force caret one space before:
577 text
.setText(current
.substring(0, pos
- 1));
579 text
.setText(current
.substring(0, pos
- 1)
580 + current
.substring(pos
));
584 // Do nothing (continue entering text)
589 if (userQuestion
.getAnswer() != null) {
590 Interactable focus
= null;
591 MainContent content
= getContent();
593 focus
= content
.nextFocus(null);
604 * Handle the input in case of "normal" (not "ask for answer") mode.
607 * the key that was pressed
609 * the answer given for this key
611 * @return if the window handled the input
613 private boolean handleKey(KeyStroke key
) {
614 boolean handled
= false;
616 if (setMessage(null, false))
619 for (KeyAction action
: actions
) {
620 if (!action
.match(key
))
625 action
.getObject(); // see {@link KeyAction#getMessage()}
626 String mess
= action
.getMessage();
628 setMessage(mess
, action
.isError());
631 if (!action
.isError() && action
.onAction()) {
632 handleAction(action
, null);
642 * Handle the input in case of "normal" (not "ask for answer") mode.
645 * the key that was pressed
647 * the answer given for this key
649 * @return if the window handled the input
651 private void handleAction(KeyAction action
, String answer
) {
652 MainContent content
= getContent();
654 switch (action
.getMode()) {
659 if (action
.getKey().getKeyType() == KeyType
.ArrowUp
)
661 if (action
.getKey().getKeyType() == KeyType
.ArrowDown
)
663 if (action
.getKey().getKeyType() == KeyType
.ArrowLeft
)
665 if (action
.getKey().getKeyType() == KeyType
.ArrowRight
)
668 if (content
!= null) {
669 String err
= content
.move(x
, y
);
671 setMessage(err
, true);
675 // mode with windows:
677 if (action
.getCard() != null) {
678 pushContent(new ContactList(action
.getCard()));
679 } else if (action
.getObject() != null
680 && action
.getObject() instanceof MainContent
) {
681 MainContent mergeContent
= (MainContent
) action
.getObject();
682 pushContent(mergeContent
);
685 case CONTACT_DETAILS
:
686 if (action
.getContact() != null) {
687 pushContent(new ContactDetails(action
.getContact()));
690 case CONTACT_DETAILS_RAW
:
691 if (action
.getContact() != null) {
692 pushContent(new ContactDetailsRaw(action
.getContact()));
695 // mode interpreted by MainWindow:
698 setMessage("Help! I need somebody! Help!", false);
702 String warning
= content
.getExitWarning();
703 if (warning
!= null) {
704 if (answer
== null) {
705 setQuestion(action
, warning
);
707 setMessage(null, false);
708 if (answer
.equalsIgnoreCase("y")) {
716 if (contentStack
.size() == 0) {
723 if (answer
== null) {
724 setQuestion(action
, action
.getQuestion(),
725 action
.getDefaultAnswer());
727 setMessage(action
.callback(answer
), true);
728 content
.refreshData();
734 if (answer
== null) {
735 setQuestion(action
, action
.getQuestion());
737 setMessage(action
.callback(answer
), true);
738 content
.refreshData();