1 package be
.nikiroo
.jvcard
.tui
;
3 import java
.util
.ArrayList
;
4 import java
.util
.Arrays
;
5 import java
.util
.LinkedList
;
8 import be
.nikiroo
.jvcard
.i18n
.Trans
;
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
.ContactDetailsRaw
;
14 import be
.nikiroo
.jvcard
.tui
.panes
.ContactList
;
15 import be
.nikiroo
.jvcard
.tui
.panes
.MainContent
;
17 import com
.googlecode
.lanterna
.TerminalSize
;
18 import com
.googlecode
.lanterna
.gui2
.BasicWindow
;
19 import com
.googlecode
.lanterna
.gui2
.BorderLayout
;
20 import com
.googlecode
.lanterna
.gui2
.Direction
;
21 import com
.googlecode
.lanterna
.gui2
.Interactable
;
22 import com
.googlecode
.lanterna
.gui2
.Label
;
23 import com
.googlecode
.lanterna
.gui2
.LinearLayout
;
24 import com
.googlecode
.lanterna
.gui2
.Panel
;
25 import com
.googlecode
.lanterna
.gui2
.TextBox
;
26 import com
.googlecode
.lanterna
.gui2
.Window
;
27 import com
.googlecode
.lanterna
.input
.KeyStroke
;
28 import com
.googlecode
.lanterna
.input
.KeyType
;
31 * This is the main "window" of the program. It can show up to one
32 * {@link MainContent} at any one time, but keeps a stack of contents.
38 public class MainWindow
extends BasicWindow
{
39 private List
<KeyAction
> defaultActions
= new LinkedList
<KeyAction
>();
40 private List
<KeyAction
> actions
= new LinkedList
<KeyAction
>();
41 private List
<MainContent
> contentStack
= new LinkedList
<MainContent
>();
42 private UserQuestion userQuestion
;
43 private String titleCache
;
44 private Panel titlePanel
;
45 private Panel mainPanel
;
46 private Panel contentPanel
;
47 private Panel actionPanel
;
48 private Panel messagePanel
;
52 * Information about a question to ask the user and its answer.
57 private class UserQuestion
{
58 private boolean oneKeyAnswer
;
59 private KeyAction action
;
60 private String answer
;
63 * Create a new {@link UserQuestion}.
66 * the action that triggered the question
68 * TRUE if we expect a one-key answer
70 public UserQuestion(KeyAction action
, boolean oneKeyAnswer
) {
72 this.oneKeyAnswer
= oneKeyAnswer
;
76 * Return the {@link KeyAction} that triggered the question.
78 * @return the {@link KeyAction}
80 public KeyAction
getAction() {
85 * Check if a one-key answer is expected.
87 * @return TRUE if a one-key answer is expected
89 public boolean isOneKeyAnswer() {
94 * Return the user answer.
96 * @return the user answer
98 public String
getAnswer() {
103 * Set the user answer.
108 public void setAnswer(String answer
) {
109 this.answer
= answer
;
114 * Create a new, empty window.
116 public MainWindow() {
121 * Create a new window hosting the given content.
124 * the content to host
126 public MainWindow(MainContent content
) {
127 super(content
== null ?
"" : content
.getTitle());
129 setHints(Arrays
.asList(Window
.Hint
.FULL_SCREEN
,
130 Window
.Hint
.NO_DECORATIONS
, Window
.Hint
.FIT_TERMINAL_WINDOW
));
132 defaultActions
.add(new KeyAction(Mode
.BACK
, 'q',
133 StringId
.KEY_ACTION_BACK
));
134 defaultActions
.add(new KeyAction(Mode
.BACK
, KeyType
.Escape
,
136 defaultActions
.add(new KeyAction(Mode
.HELP
, 'h',
137 StringId
.KEY_ACTION_HELP
));
138 defaultActions
.add(new KeyAction(Mode
.HELP
, KeyType
.F1
, StringId
.NULL
));
140 actionPanel
= new Panel();
141 contentPanel
= new Panel();
142 mainPanel
= new Panel();
143 messagePanel
= new Panel();
144 titlePanel
= new Panel();
146 Panel actionMessagePanel
= new Panel();
148 LinearLayout llayout
= new LinearLayout(Direction
.HORIZONTAL
);
149 llayout
.setSpacing(0);
150 actionPanel
.setLayoutManager(llayout
);
152 BorderLayout blayout
= new BorderLayout();
153 titlePanel
.setLayoutManager(blayout
);
155 llayout
= new LinearLayout(Direction
.VERTICAL
);
156 llayout
.setSpacing(0);
157 messagePanel
.setLayoutManager(llayout
);
159 blayout
= new BorderLayout();
160 mainPanel
.setLayoutManager(blayout
);
162 blayout
= new BorderLayout();
163 contentPanel
.setLayoutManager(blayout
);
165 blayout
= new BorderLayout();
166 actionMessagePanel
.setLayoutManager(blayout
);
169 .addComponent(messagePanel
, BorderLayout
.Location
.TOP
);
170 actionMessagePanel
.addComponent(actionPanel
,
171 BorderLayout
.Location
.CENTER
);
173 mainPanel
.addComponent(titlePanel
, BorderLayout
.Location
.TOP
);
174 mainPanel
.addComponent(contentPanel
, BorderLayout
.Location
.CENTER
);
176 .addComponent(actionMessagePanel
, BorderLayout
.Location
.BOTTOM
);
178 pushContent(content
);
180 setComponent(mainPanel
);
184 * "push" some content to the window stack.
187 * the new top-of-the-stack content
189 public void pushContent(MainContent content
) {
190 List
<KeyAction
> actions
= null;
192 contentPanel
.removeAllComponents();
193 if (content
!= null) {
194 actions
= content
.getKeyBindings();
195 contentPanel
.addComponent(content
, BorderLayout
.Location
.CENTER
);
196 this.contentStack
.add(content
);
198 Interactable focus
= content
.nextFocus(null);
204 setActions(actions
, true);
208 * "pop" the latest, top-of-the-stack content from the window stack.
210 * @return the removed content if any
212 public MainContent
popContent() {
213 MainContent removed
= null;
214 MainContent prev
= null;
216 MainContent content
= getContent();
218 removed
= contentStack
.remove(contentStack
.size() - 1);
220 if (contentStack
.size() > 0)
221 prev
= contentStack
.remove(contentStack
.size() - 1);
229 * Show the given message on screen. It will disappear at the next action.
232 * the message to display
234 * TRUE for an error message, FALSE for an information message
236 * @return TRUE if changes were performed
238 public boolean setMessage(String mess
, boolean error
) {
239 if (mess
!= null || messagePanel
.getChildCount() > 0) {
240 messagePanel
.removeAllComponents();
242 Element element
= (error ? UiColors
.Element
.LINE_MESSAGE_ERR
243 : UiColors
.Element
.LINE_MESSAGE
);
244 Label lbl
= element
.createLabel(" " + mess
+ " ");
245 messagePanel
.addComponent(lbl
, LinearLayout
246 .createLayoutData(LinearLayout
.Alignment
.Center
));
255 * Show a question to the user and switch to "ask for answer" mode see
256 * {@link MainWindow#handleQuestion}. The user will be asked to enter some
257 * answer and confirm with ENTER.
262 * the question to ask
264 * the initial answer if any (to be edited by the user)
266 public void setQuestion(KeyAction action
, String question
, String initial
) {
267 setQuestion(action
, question
, initial
, false);
271 * Show a question to the user and switch to "ask for answer" mode see
272 * {@link MainWindow#handleQuestion}. The user will be asked to hit one key
278 * the question to ask
280 public void setQuestion(KeyAction action
, String question
) {
281 setQuestion(action
, question
, null, true);
285 * Show a question to the user and switch to "ask for answer" mode see
286 * {@link MainWindow#handleQuestion}.
291 * the question to ask
293 * the initial answer if any (to be edited by the user)
295 * TRUE for a one-key answer, FALSE for a text answer validated
298 private void setQuestion(KeyAction action
, String question
, String initial
,
300 userQuestion
= new UserQuestion(action
, oneKey
);
302 messagePanel
.removeAllComponents();
304 Panel hpanel
= new Panel();
305 LinearLayout llayout
= new LinearLayout(Direction
.HORIZONTAL
);
306 llayout
.setSpacing(0);
307 hpanel
.setLayoutManager(llayout
);
309 Label lbl
= UiColors
.Element
.LINE_MESSAGE_QUESTION
.createLabel(" "
311 text
= new TextBox(new TerminalSize(getSize().getColumns()
312 - lbl
.getSize().getColumns(), 1));
314 if (initial
!= null) {
315 // add all chars one by one so the caret is at the end
316 for (int index
= 0; index
< initial
.length(); index
++) {
317 text
.handleInput(new KeyStroke(initial
.charAt(index
), false,
322 hpanel
.addComponent(lbl
,
323 LinearLayout
.createLayoutData(LinearLayout
.Alignment
.Beginning
));
324 hpanel
.addComponent(text
,
325 LinearLayout
.createLayoutData(LinearLayout
.Alignment
.Fill
));
328 .addComponent(hpanel
, LinearLayout
329 .createLayoutData(LinearLayout
.Alignment
.Beginning
));
335 * Refresh the window and the empty-space handling. You should call this
336 * method when the window size changed.
339 * the new size of the window
341 public void refresh(TerminalSize size
) {
345 if (getSize() == null || !getSize().equals(size
))
351 setActions(new ArrayList
<KeyAction
>(actions
), false);
357 public void invalidate() {
359 for (MainContent content
: contentStack
) {
360 content
.invalidate();
365 public boolean handleInput(KeyStroke key
) {
366 boolean handled
= false;
368 if (userQuestion
!= null) {
369 handled
= handleQuestion(userQuestion
, key
);
371 if (userQuestion
.getAnswer() != null) {
372 handleAction(userQuestion
.getAction(),
373 userQuestion
.getAnswer());
379 handled
= handleKey(key
);
383 handled
= super.handleInput(key
);
390 * Actually set the title <b>inside</b> the window. Will also call
391 * {@link BasicWindow#setTitle} with the computed parameters.
393 private void setTitle() {
394 String prefix
= " " + Main
.APPLICATION_TITLE
+ " (version "
395 + Main
.APPLICATION_VERSION
+ ")";
400 MainContent content
= getContent();
401 if (content
!= null) {
402 title
= content
.getTitle();
403 count
= content
.getCount();
409 if (title
.length() > 0) {
410 prefix
= prefix
+ ": ";
411 title
= StringUtils
.sanitize(title
, UiColors
.getInstance()
415 String countStr
= "";
417 countStr
= "[" + count
+ "]";
421 if (getSize() != null) {
422 width
= getSize().getColumns();
426 int padding
= width
- prefix
.length() - title
.length()
429 if (title
.length() > 0)
430 title
= StringUtils
.padString(title
, title
.length()
433 prefix
= StringUtils
.padString(prefix
, prefix
.length()
438 String titleCache
= prefix
+ title
+ count
;
439 if (!titleCache
.equals(this.titleCache
)) {
440 super.setTitle(prefix
);
442 Label lblPrefix
= new Label(prefix
);
443 UiColors
.Element
.TITLE_MAIN
.themeLabel(lblPrefix
);
445 Label lblTitle
= null;
446 if (title
.length() > 0) {
447 lblTitle
= new Label(title
);
448 UiColors
.Element
.TITLE_VARIABLE
.themeLabel(lblTitle
);
451 Label lblCount
= null;
452 if (countStr
!= null) {
453 lblCount
= new Label(countStr
);
454 UiColors
.Element
.TITLE_COUNT
.themeLabel(lblCount
);
457 titlePanel
.removeAllComponents();
459 titlePanel
.addComponent(lblPrefix
, BorderLayout
.Location
.LEFT
);
460 if (lblTitle
!= null)
461 titlePanel
.addComponent(lblTitle
, BorderLayout
.Location
.CENTER
);
462 if (lblCount
!= null)
463 titlePanel
.addComponent(lblCount
, BorderLayout
.Location
.RIGHT
);
468 * Return the current {@link MainContent} from the stack if any.
470 * @return the current {@link MainContent}
472 private MainContent
getContent() {
473 if (contentStack
.size() > 0) {
474 return contentStack
.get(contentStack
.size() - 1);
481 * Update the list of actions and refresh the action panel.
484 * the list of actions to support
485 * @param enableDefaultactions
486 * TRUE to enable the default actions
488 private void setActions(List
<KeyAction
> actions
,
489 boolean enableDefaultactions
) {
490 this.actions
.clear();
492 if (enableDefaultactions
)
493 this.actions
.addAll(defaultActions
);
496 this.actions
.addAll(actions
);
498 actionPanel
.removeAllComponents();
499 for (KeyAction action
: this.actions
) {
500 String trans
= " " + action
.getStringId().trans() + " ";
502 if (" ".equals(trans
))
505 String keyTrans
= Trans
.getInstance().trans(action
.getKey());
507 Panel kPane
= new Panel();
508 LinearLayout layout
= new LinearLayout(Direction
.HORIZONTAL
);
509 layout
.setSpacing(0);
510 kPane
.setLayoutManager(layout
);
512 kPane
.addComponent(UiColors
.Element
.ACTION_KEY
513 .createLabel(keyTrans
));
514 kPane
.addComponent(UiColors
.Element
.ACTION_DESC
.createLabel(trans
));
516 actionPanel
.addComponent(kPane
);
519 // fill with "desc" colour
521 if (getSize() != null) {
522 width
= getSize().getColumns();
526 actionPanel
.addComponent(UiColors
.Element
.ACTION_DESC
527 .createLabel(StringUtils
.padString("", width
)));
532 * Handle user input when in "ask for question" mode (see
533 * {@link MainWindow#userQuestion}).
535 * @param userQuestion
538 * the key that has been pressed by the user
540 * @return TRUE if the {@link KeyStroke} was handled
542 private boolean handleQuestion(UserQuestion userQuestion
, KeyStroke key
) {
543 userQuestion
.setAnswer(null);
545 if (userQuestion
.isOneKeyAnswer()) {
546 userQuestion
.setAnswer("" + key
.getCharacter());
549 if (key
.isCtrlDown() && key
.getCharacter() == 'h') {
550 key
= new KeyStroke(KeyType
.Backspace
);
553 switch (key
.getKeyType()) {
556 userQuestion
.setAnswer(text
.getText());
558 userQuestion
.setAnswer("");
561 int pos
= text
.getCaretPosition().getColumn();
563 String current
= text
.getText();
564 // force caret one space before:
565 text
.setText(current
.substring(0, pos
- 1));
567 text
.setText(current
.substring(0, pos
- 1)
568 + current
.substring(pos
));
572 // Do nothing (continue entering text)
577 if (userQuestion
.getAnswer() != null) {
578 Interactable focus
= null;
579 MainContent content
= getContent();
581 focus
= content
.nextFocus(null);
592 * Handle the input in case of "normal" (not "ask for answer") mode.
595 * the key that was pressed
597 * the answer given for this key
599 * @return if the window handled the input
601 private boolean handleKey(KeyStroke key
) {
602 boolean handled
= false;
604 if (setMessage(null, false))
607 for (KeyAction action
: actions
) {
608 if (!action
.match(key
))
613 if (action
.onAction()) {
614 handleAction(action
, null);
624 * Handle the input in case of "normal" (not "ask for answer") mode.
627 * the key that was pressed
629 * the answer given for this key
631 * @return if the window handled the input
633 private void handleAction(KeyAction action
, String answer
) {
634 MainContent content
= getContent();
636 switch (action
.getMode()) {
641 if (action
.getKey().getKeyType() == KeyType
.ArrowUp
)
643 if (action
.getKey().getKeyType() == KeyType
.ArrowDown
)
645 if (action
.getKey().getKeyType() == KeyType
.ArrowLeft
)
647 if (action
.getKey().getKeyType() == KeyType
.ArrowRight
)
650 if (content
!= null) {
651 String err
= content
.move(x
, y
);
653 setMessage(err
, true);
657 // mode with windows:
659 if (action
.getCard() != null) {
660 pushContent(new ContactList(action
.getCard()));
663 case CONTACT_DETAILS
:
664 if (action
.getContact() != null) {
665 pushContent(new ContactDetails(action
.getContact()));
668 case CONTACT_DETAILS_RAW
:
669 if (action
.getContact() != null) {
670 pushContent(new ContactDetailsRaw(action
.getContact()));
673 // mode interpreted by MainWindow:
676 setMessage("Help! I need somebody! Help!", false);
680 String warning
= content
.getExitWarning();
681 if (warning
!= null) {
682 if (answer
== null) {
683 setQuestion(action
, warning
);
685 setMessage(null, false);
686 if (answer
.equalsIgnoreCase("y")) {
694 if (contentStack
.size() == 0) {
701 if (answer
== null) {
702 setQuestion(action
, action
.getQuestion(),
703 action
.getDefaultAnswer());
705 setMessage(action
.callback(answer
), true);
706 content
.refreshData();
712 if (answer
== null) {
713 setQuestion(action
, action
.getQuestion());
715 setMessage(action
.callback(answer
), true);
716 content
.refreshData();