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
.StringId
;
9 import be
.nikiroo
.jvcard
.tui
.KeyAction
.Mode
;
10 import be
.nikiroo
.jvcard
.tui
.UiColors
.Element
;
11 import be
.nikiroo
.jvcard
.tui
.panes
.ContactDetails
;
12 import be
.nikiroo
.jvcard
.tui
.panes
.ContactDetailsRaw
;
13 import be
.nikiroo
.jvcard
.tui
.panes
.ContactList
;
14 import be
.nikiroo
.jvcard
.tui
.panes
.MainContent
;
16 import com
.googlecode
.lanterna
.TerminalSize
;
17 import com
.googlecode
.lanterna
.gui2
.BasicWindow
;
18 import com
.googlecode
.lanterna
.gui2
.BorderLayout
;
19 import com
.googlecode
.lanterna
.gui2
.Direction
;
20 import com
.googlecode
.lanterna
.gui2
.Interactable
;
21 import com
.googlecode
.lanterna
.gui2
.Label
;
22 import com
.googlecode
.lanterna
.gui2
.LinearLayout
;
23 import com
.googlecode
.lanterna
.gui2
.Panel
;
24 import com
.googlecode
.lanterna
.gui2
.TextBox
;
25 import com
.googlecode
.lanterna
.gui2
.Window
;
26 import com
.googlecode
.lanterna
.input
.KeyStroke
;
27 import com
.googlecode
.lanterna
.input
.KeyType
;
30 * This is the main "window" of the program. It can show up to one
31 * {@link MainContent} at any one time, but keeps a stack of contents.
37 public class MainWindow
extends BasicWindow
{
38 private List
<KeyAction
> defaultActions
= new LinkedList
<KeyAction
>();
39 private List
<KeyAction
> actions
= new LinkedList
<KeyAction
>();
40 private List
<MainContent
> contentStack
= new LinkedList
<MainContent
>();
41 private UserQuestion userQuestion
;
42 private String titleCache
;
43 private Panel titlePanel
;
44 private Panel mainPanel
;
45 private Panel contentPanel
;
46 private Panel actionPanel
;
47 private Panel messagePanel
;
51 * Information about a question to ask the user and its answer.
56 private class UserQuestion
{
57 private boolean oneKeyAnswer
;
58 private KeyAction action
;
59 private String answer
;
62 * Create a new {@link UserQuestion}.
65 * the action that triggered the question
67 * TRUE if we expect a one-key answer
69 public UserQuestion(KeyAction action
, boolean oneKeyAnswer
) {
71 this.oneKeyAnswer
= oneKeyAnswer
;
75 * Return the {@link KeyAction} that triggered the question.
77 * @return the {@link KeyAction}
79 public KeyAction
getAction() {
84 * Check if a one-key answer is expected.
86 * @return TRUE if a one-key answer is expected
88 public boolean isOneKeyAnswer() {
93 * Return the user answer.
95 * @return the user answer
97 public String
getAnswer() {
102 * Set the user answer.
107 public void setAnswer(String answer
) {
108 this.answer
= answer
;
113 * Create a new, empty window.
115 public MainWindow() {
120 * Create a new window hosting the given content.
123 * the content to host
125 public MainWindow(MainContent content
) {
126 super(content
== null ?
"" : content
.getTitle());
128 setHints(Arrays
.asList(Window
.Hint
.FULL_SCREEN
,
129 Window
.Hint
.NO_DECORATIONS
, Window
.Hint
.FIT_TERMINAL_WINDOW
));
131 defaultActions
.add(new KeyAction(Mode
.BACK
, 'q',
132 StringId
.KEY_ACTION_BACK
));
133 defaultActions
.add(new KeyAction(Mode
.BACK
, KeyType
.Escape
,
135 defaultActions
.add(new KeyAction(Mode
.HELP
, 'h',
136 StringId
.KEY_ACTION_HELP
));
137 defaultActions
.add(new KeyAction(Mode
.HELP
, KeyType
.F1
, StringId
.NULL
));
139 actionPanel
= new Panel();
140 contentPanel
= new Panel();
141 mainPanel
= new Panel();
142 messagePanel
= new Panel();
143 titlePanel
= new Panel();
145 Panel actionMessagePanel
= new Panel();
147 LinearLayout llayout
= new LinearLayout(Direction
.HORIZONTAL
);
148 llayout
.setSpacing(0);
149 actionPanel
.setLayoutManager(llayout
);
151 BorderLayout blayout
= new BorderLayout();
152 titlePanel
.setLayoutManager(blayout
);
154 llayout
= new LinearLayout(Direction
.VERTICAL
);
155 llayout
.setSpacing(0);
156 messagePanel
.setLayoutManager(llayout
);
158 blayout
= new BorderLayout();
159 mainPanel
.setLayoutManager(blayout
);
161 blayout
= new BorderLayout();
162 contentPanel
.setLayoutManager(blayout
);
164 blayout
= new BorderLayout();
165 actionMessagePanel
.setLayoutManager(blayout
);
168 .addComponent(messagePanel
, BorderLayout
.Location
.TOP
);
169 actionMessagePanel
.addComponent(actionPanel
,
170 BorderLayout
.Location
.CENTER
);
172 mainPanel
.addComponent(titlePanel
, BorderLayout
.Location
.TOP
);
173 mainPanel
.addComponent(contentPanel
, BorderLayout
.Location
.CENTER
);
175 .addComponent(actionMessagePanel
, BorderLayout
.Location
.BOTTOM
);
177 pushContent(content
);
179 setComponent(mainPanel
);
183 * "push" some content to the window stack.
186 * the new top-of-the-stack content
188 public void pushContent(MainContent content
) {
189 List
<KeyAction
> actions
= null;
191 contentPanel
.removeAllComponents();
192 if (content
!= null) {
193 actions
= content
.getKeyBindings();
194 contentPanel
.addComponent(content
, BorderLayout
.Location
.CENTER
);
195 this.contentStack
.add(content
);
197 Interactable focus
= content
.nextFocus(null);
203 setActions(actions
, true);
207 * "pop" the latest, top-of-the-stack content from the window stack.
209 * @return the removed content if any
211 public MainContent
popContent() {
212 MainContent removed
= null;
213 MainContent prev
= null;
215 MainContent content
= getContent();
217 removed
= contentStack
.remove(contentStack
.size() - 1);
219 if (contentStack
.size() > 0)
220 prev
= contentStack
.remove(contentStack
.size() - 1);
228 * Show the given message on screen. It will disappear at the next action.
231 * the message to display
233 * TRUE for an error message, FALSE for an information message
235 * @return TRUE if changes were performed
237 public boolean setMessage(String mess
, boolean error
) {
238 if (mess
!= null || messagePanel
.getChildCount() > 0) {
239 messagePanel
.removeAllComponents();
241 Element element
= (error ? UiColors
.Element
.LINE_MESSAGE_ERR
242 : UiColors
.Element
.LINE_MESSAGE
);
243 Label lbl
= element
.createLabel(" " + mess
+ " ");
244 messagePanel
.addComponent(lbl
, LinearLayout
245 .createLayoutData(LinearLayout
.Alignment
.Center
));
254 * Show a question to the user and switch to "ask for answer" mode see
255 * {@link MainWindow#handleQuestion}. The user will be asked to enter some
256 * answer and confirm with ENTER.
261 * the question to ask
263 * the initial answer if any (to be edited by the user)
265 public void setQuestion(KeyAction action
, String question
, String initial
) {
266 setQuestion(action
, question
, initial
, false);
270 * Show a question to the user and switch to "ask for answer" mode see
271 * {@link MainWindow#handleQuestion}. The user will be asked to hit one key
277 * the question to ask
279 public void setQuestion(KeyAction action
, String question
) {
280 setQuestion(action
, question
, null, true);
284 * Show a question to the user and switch to "ask for answer" mode see
285 * {@link MainWindow#handleQuestion}.
290 * the question to ask
292 * the initial answer if any (to be edited by the user)
294 * TRUE for a one-key answer, FALSE for a text answer validated
297 private void setQuestion(KeyAction action
, String question
, String initial
,
299 userQuestion
= new UserQuestion(action
, oneKey
);
301 messagePanel
.removeAllComponents();
303 Panel hpanel
= new Panel();
304 LinearLayout llayout
= new LinearLayout(Direction
.HORIZONTAL
);
305 llayout
.setSpacing(0);
306 hpanel
.setLayoutManager(llayout
);
308 Label lbl
= UiColors
.Element
.LINE_MESSAGE_QUESTION
.createLabel(" "
310 text
= new TextBox(new TerminalSize(getSize().getColumns()
311 - lbl
.getSize().getColumns(), 1));
313 if (initial
!= null) {
314 // add all chars one by one so the caret is at the end
315 for (int index
= 0; index
< initial
.length(); index
++) {
316 text
.handleInput(new KeyStroke(initial
.charAt(index
), false,
321 hpanel
.addComponent(lbl
,
322 LinearLayout
.createLayoutData(LinearLayout
.Alignment
.Beginning
));
323 hpanel
.addComponent(text
,
324 LinearLayout
.createLayoutData(LinearLayout
.Alignment
.Fill
));
327 .addComponent(hpanel
, LinearLayout
328 .createLayoutData(LinearLayout
.Alignment
.Beginning
));
334 * Refresh the window and the empty-space handling. You should call this
335 * method when the window size changed.
338 * the new size of the window
340 public void refresh(TerminalSize size
) {
344 if (getSize() == null || !getSize().equals(size
))
350 setActions(new ArrayList
<KeyAction
>(actions
), false);
356 public void invalidate() {
358 for (MainContent content
: contentStack
) {
359 content
.invalidate();
364 public boolean handleInput(KeyStroke key
) {
365 boolean handled
= false;
367 if (userQuestion
!= null) {
368 handled
= handleQuestion(userQuestion
, key
);
370 if (userQuestion
.getAnswer() != null) {
371 handleAction(userQuestion
.getAction(),
372 userQuestion
.getAnswer());
378 handled
= handleKey(key
);
382 handled
= super.handleInput(key
);
389 * Actually set the title <b>inside</b> the window. Will also call
390 * {@link BasicWindow#setTitle} with the computed parameters.
392 private void setTitle() {
393 String prefix
= " " + Main
.APPLICATION_TITLE
+ " (version "
394 + Main
.APPLICATION_VERSION
+ ")";
399 MainContent content
= getContent();
400 if (content
!= null) {
401 title
= content
.getTitle();
402 count
= content
.getCount();
408 if (title
.length() > 0) {
409 prefix
= prefix
+ ": ";
410 title
= StringUtils
.sanitize(title
, UiColors
.getInstance()
414 String countStr
= "";
416 countStr
= "[" + count
+ "]";
420 if (getSize() != null) {
421 width
= getSize().getColumns();
425 int padding
= width
- prefix
.length() - title
.length()
428 if (title
.length() > 0)
429 title
= StringUtils
.padString(title
, title
.length()
432 prefix
= StringUtils
.padString(prefix
, prefix
.length()
437 String titleCache
= prefix
+ title
+ count
;
438 if (!titleCache
.equals(this.titleCache
)) {
439 super.setTitle(prefix
);
441 Label lblPrefix
= new Label(prefix
);
442 UiColors
.Element
.TITLE_MAIN
.themeLabel(lblPrefix
);
444 Label lblTitle
= null;
445 if (title
.length() > 0) {
446 lblTitle
= new Label(title
);
447 UiColors
.Element
.TITLE_VARIABLE
.themeLabel(lblTitle
);
450 Label lblCount
= null;
451 if (countStr
!= null) {
452 lblCount
= new Label(countStr
);
453 UiColors
.Element
.TITLE_COUNT
.themeLabel(lblCount
);
456 titlePanel
.removeAllComponents();
458 titlePanel
.addComponent(lblPrefix
, BorderLayout
.Location
.LEFT
);
459 if (lblTitle
!= null)
460 titlePanel
.addComponent(lblTitle
, BorderLayout
.Location
.CENTER
);
461 if (lblCount
!= null)
462 titlePanel
.addComponent(lblCount
, BorderLayout
.Location
.RIGHT
);
467 * Return the current {@link MainContent} from the stack if any.
469 * @return the current {@link MainContent}
471 private MainContent
getContent() {
472 if (contentStack
.size() > 0) {
473 return contentStack
.get(contentStack
.size() - 1);
480 * Update the list of actions and refresh the action panel.
483 * the list of actions to support
484 * @param enableDefaultactions
485 * TRUE to enable the default actions
487 private void setActions(List
<KeyAction
> actions
,
488 boolean enableDefaultactions
) {
489 this.actions
.clear();
491 if (enableDefaultactions
)
492 this.actions
.addAll(defaultActions
);
495 this.actions
.addAll(actions
);
497 actionPanel
.removeAllComponents();
498 for (KeyAction action
: this.actions
) {
499 String trans
= " " + Main
.trans(action
.getStringId()) + " ";
501 if (" ".equals(trans
))
504 String keyTrans
= Main
.trans(action
.getKey());
506 Panel kPane
= new Panel();
507 LinearLayout layout
= new LinearLayout(Direction
.HORIZONTAL
);
508 layout
.setSpacing(0);
509 kPane
.setLayoutManager(layout
);
511 kPane
.addComponent(UiColors
.Element
.ACTION_KEY
512 .createLabel(keyTrans
));
513 kPane
.addComponent(UiColors
.Element
.ACTION_DESC
.createLabel(trans
));
515 actionPanel
.addComponent(kPane
);
518 // fill with "desc" colour
520 if (getSize() != null) {
521 width
= getSize().getColumns();
525 actionPanel
.addComponent(UiColors
.Element
.ACTION_DESC
526 .createLabel(StringUtils
.padString("", width
)));
531 * Handle user input when in "ask for question" mode (see
532 * {@link MainWindow#userQuestion}).
534 * @param userQuestion
537 * the key that has been pressed by the user
539 * @return TRUE if the {@link KeyStroke} was handled
541 private boolean handleQuestion(UserQuestion userQuestion
, KeyStroke key
) {
542 userQuestion
.setAnswer(null);
544 if (userQuestion
.isOneKeyAnswer()) {
545 userQuestion
.setAnswer("" + key
.getCharacter());
548 if (key
.isCtrlDown() && key
.getCharacter() == 'h') {
549 key
= new KeyStroke(KeyType
.Backspace
);
552 switch (key
.getKeyType()) {
555 userQuestion
.setAnswer(text
.getText());
557 userQuestion
.setAnswer("");
560 int pos
= text
.getCaretPosition().getColumn();
562 String current
= text
.getText();
563 // force caret one space before:
564 text
.setText(current
.substring(0, pos
- 1));
566 text
.setText(current
.substring(0, pos
- 1)
567 + current
.substring(pos
));
571 // Do nothing (continue entering text)
576 if (userQuestion
.getAnswer() != null) {
577 Interactable focus
= null;
578 MainContent content
= getContent();
580 focus
= content
.nextFocus(null);
591 * Handle the input in case of "normal" (not "ask for answer") mode.
594 * the key that was pressed
596 * the answer given for this key
598 * @return if the window handled the input
600 private boolean handleKey(KeyStroke key
) {
601 boolean handled
= false;
603 if (setMessage(null, false))
606 for (KeyAction action
: actions
) {
607 if (!action
.match(key
))
612 if (action
.onAction()) {
613 handleAction(action
, null);
623 * Handle the input in case of "normal" (not "ask for answer") mode.
626 * the key that was pressed
628 * the answer given for this key
630 * @return if the window handled the input
632 private void handleAction(KeyAction action
, String answer
) {
633 MainContent content
= getContent();
635 switch (action
.getMode()) {
640 if (action
.getKey().getKeyType() == KeyType
.ArrowUp
)
642 if (action
.getKey().getKeyType() == KeyType
.ArrowDown
)
644 if (action
.getKey().getKeyType() == KeyType
.ArrowLeft
)
646 if (action
.getKey().getKeyType() == KeyType
.ArrowRight
)
649 if (content
!= null) {
650 String err
= content
.move(x
, y
);
652 setMessage(err
, true);
656 // mode with windows:
658 if (action
.getCard() != null) {
659 pushContent(new ContactList(action
.getCard()));
662 case CONTACT_DETAILS
:
663 if (action
.getContact() != null) {
664 pushContent(new ContactDetails(action
.getContact()));
667 case CONTACT_DETAILS_RAW
:
668 if (action
.getContact() != null) {
669 pushContent(new ContactDetailsRaw(action
.getContact()));
672 // mode interpreted by MainWindow:
675 setMessage("Help! I need somebody! Help!", false);
679 String warning
= content
.getExitWarning();
680 if (warning
!= null) {
681 if (answer
== null) {
682 setQuestion(action
, warning
);
684 setMessage(null, false);
685 if (answer
.equalsIgnoreCase("y")) {
693 if (contentStack
.size() == 0) {
700 if (answer
== null) {
701 setQuestion(action
, action
.getQuestion(),
702 action
.getDefaultAnswer());
704 setMessage(action
.callback(answer
), true);
705 content
.refreshData();
711 if (answer
== null) {
712 setQuestion(action
, action
.getQuestion());
714 setMessage(action
.callback(answer
), true);
715 content
.refreshData();