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
.launcher
.Main
;
9 import be
.nikiroo
.jvcard
.resources
.StringUtils
;
10 import be
.nikiroo
.jvcard
.resources
.Trans
.StringId
;
11 import be
.nikiroo
.jvcard
.tui
.KeyAction
.Mode
;
12 import be
.nikiroo
.jvcard
.tui
.UiColors
.Element
;
13 import be
.nikiroo
.jvcard
.tui
.panes
.ContactDetails
;
14 import be
.nikiroo
.jvcard
.tui
.panes
.ContactDetailsRaw
;
15 import be
.nikiroo
.jvcard
.tui
.panes
.ContactList
;
16 import be
.nikiroo
.jvcard
.tui
.panes
.MainContent
;
18 import com
.googlecode
.lanterna
.TerminalSize
;
19 import com
.googlecode
.lanterna
.gui2
.BasicWindow
;
20 import com
.googlecode
.lanterna
.gui2
.BorderLayout
;
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
.Window
;
28 import com
.googlecode
.lanterna
.input
.KeyStroke
;
29 import com
.googlecode
.lanterna
.input
.KeyType
;
32 * This is the main "window" of the program. It can show up to one
33 * {@link MainContent} at any one time, but keeps a stack of contents.
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 UserQuestion userQuestion
;
44 private String titleCache
;
45 private Panel titlePanel
;
46 private Panel mainPanel
;
47 private Panel contentPanel
;
48 private Panel actionPanel
;
49 private Panel messagePanel
;
53 * Information about a question to ask the user and its answer.
58 private class UserQuestion
{
59 private boolean oneKeyAnswer
;
60 private KeyAction action
;
61 private String answer
;
64 * Create a new {@link UserQuestion}.
67 * the action that triggered the question
69 * TRUE if we expect a one-key answer
71 public UserQuestion(KeyAction action
, boolean oneKeyAnswer
) {
73 this.oneKeyAnswer
= oneKeyAnswer
;
77 * Return the {@link KeyAction} that triggered the question.
79 * @return the {@link KeyAction}
81 public KeyAction
getAction() {
86 * Check if a one-key answer is expected.
88 * @return TRUE if a one-key answer is expected
90 public boolean isOneKeyAnswer() {
95 * Return the user answer.
97 * @return the user answer
99 public String
getAnswer() {
104 * Set the user answer.
109 public void setAnswer(String answer
) {
110 this.answer
= answer
;
115 * Create a new, empty window.
117 public MainWindow() {
122 * Create a new window hosting the given content.
125 * the content to host
127 public MainWindow(MainContent content
) {
128 super(content
== null ?
"" : content
.getTitle());
130 setHints(Arrays
.asList(Window
.Hint
.FULL_SCREEN
,
131 Window
.Hint
.NO_DECORATIONS
, Window
.Hint
.FIT_TERMINAL_WINDOW
));
133 defaultActions
.add(new KeyAction(Mode
.BACK
, 'q',
134 StringId
.KEY_ACTION_BACK
));
135 defaultActions
.add(new KeyAction(Mode
.BACK
, KeyType
.Escape
,
137 defaultActions
.add(new KeyAction(Mode
.HELP
, 'h',
138 StringId
.KEY_ACTION_HELP
));
139 defaultActions
.add(new KeyAction(Mode
.HELP
, KeyType
.F1
, StringId
.NULL
));
141 actionPanel
= new Panel();
142 contentPanel
= new Panel();
143 mainPanel
= new Panel();
144 messagePanel
= new Panel();
145 titlePanel
= new Panel();
147 Panel actionMessagePanel
= new Panel();
149 LinearLayout llayout
= new LinearLayout(Direction
.HORIZONTAL
);
150 llayout
.setSpacing(0);
151 actionPanel
.setLayoutManager(llayout
);
153 BorderLayout blayout
= new BorderLayout();
154 titlePanel
.setLayoutManager(blayout
);
156 llayout
= new LinearLayout(Direction
.VERTICAL
);
157 llayout
.setSpacing(0);
158 messagePanel
.setLayoutManager(llayout
);
160 blayout
= new BorderLayout();
161 mainPanel
.setLayoutManager(blayout
);
163 blayout
= new BorderLayout();
164 contentPanel
.setLayoutManager(blayout
);
166 blayout
= new BorderLayout();
167 actionMessagePanel
.setLayoutManager(blayout
);
170 .addComponent(messagePanel
, BorderLayout
.Location
.TOP
);
171 actionMessagePanel
.addComponent(actionPanel
,
172 BorderLayout
.Location
.CENTER
);
174 mainPanel
.addComponent(titlePanel
, BorderLayout
.Location
.TOP
);
175 mainPanel
.addComponent(contentPanel
, BorderLayout
.Location
.CENTER
);
177 .addComponent(actionMessagePanel
, BorderLayout
.Location
.BOTTOM
);
179 pushContent(content
);
181 setComponent(mainPanel
);
185 * "push" some content to the window stack.
188 * the new top-of-the-stack content
190 public void pushContent(MainContent content
) {
191 List
<KeyAction
> actions
= null;
193 contentPanel
.removeAllComponents();
194 if (content
!= null) {
195 actions
= content
.getKeyBindings();
196 contentPanel
.addComponent(content
, BorderLayout
.Location
.CENTER
);
197 this.contentStack
.add(content
);
199 Interactable focus
= content
.nextFocus(null);
205 setActions(actions
, true);
209 * "pop" the latest, top-of-the-stack content from the window stack.
211 * @return the removed content if any
213 public MainContent
popContent() {
214 MainContent removed
= null;
215 MainContent prev
= null;
217 MainContent content
= getContent();
219 removed
= contentStack
.remove(contentStack
.size() - 1);
221 if (contentStack
.size() > 0)
222 prev
= contentStack
.remove(contentStack
.size() - 1);
230 * Show the given message on screen. It will disappear at the next action.
233 * the message to display
235 * TRUE for an error message, FALSE for an information message
237 * @return TRUE if changes were performed
239 public boolean setMessage(String mess
, boolean error
) {
240 if (mess
!= null || messagePanel
.getChildCount() > 0) {
241 messagePanel
.removeAllComponents();
243 Element element
= (error ? UiColors
.Element
.LINE_MESSAGE_ERR
244 : UiColors
.Element
.LINE_MESSAGE
);
245 Label lbl
= element
.createLabel(" " + mess
+ " ");
246 messagePanel
.addComponent(lbl
, LinearLayout
247 .createLayoutData(LinearLayout
.Alignment
.Center
));
256 * Show a question to the user and switch to "ask for answer" mode see
257 * {@link MainWindow#handleQuestion}. The user will be asked to enter some
258 * answer and confirm with ENTER.
263 * the question to ask
265 * the initial answer if any (to be edited by the user)
267 public void setQuestion(KeyAction action
, String question
, String initial
) {
268 setQuestion(action
, question
, initial
, false);
272 * Show a question to the user and switch to "ask for answer" mode see
273 * {@link MainWindow#handleQuestion}. The user will be asked to hit one key
279 * the question to ask
281 public void setQuestion(KeyAction action
, String question
) {
282 setQuestion(action
, question
, null, true);
286 * Show a question to the user and switch to "ask for answer" mode see
287 * {@link MainWindow#handleQuestion}.
292 * the question to ask
294 * the initial answer if any (to be edited by the user)
296 * TRUE for a one-key answer, FALSE for a text answer validated
299 private void setQuestion(KeyAction action
, String question
, String initial
,
301 userQuestion
= new UserQuestion(action
, oneKey
);
303 messagePanel
.removeAllComponents();
305 Panel hpanel
= new Panel();
306 LinearLayout llayout
= new LinearLayout(Direction
.HORIZONTAL
);
307 llayout
.setSpacing(0);
308 hpanel
.setLayoutManager(llayout
);
310 Label lbl
= UiColors
.Element
.LINE_MESSAGE_QUESTION
.createLabel(" "
312 text
= new TextBox(new TerminalSize(getSize().getColumns()
313 - lbl
.getSize().getColumns(), 1));
315 if (initial
!= null) {
316 // add all chars one by one so the caret is at the end
317 for (int index
= 0; index
< initial
.length(); index
++) {
318 text
.handleInput(new KeyStroke(initial
.charAt(index
), false,
323 hpanel
.addComponent(lbl
,
324 LinearLayout
.createLayoutData(LinearLayout
.Alignment
.Beginning
));
325 hpanel
.addComponent(text
,
326 LinearLayout
.createLayoutData(LinearLayout
.Alignment
.Fill
));
329 .addComponent(hpanel
, LinearLayout
330 .createLayoutData(LinearLayout
.Alignment
.Beginning
));
336 * Refresh the window and the empty-space handling. You should call this
337 * method when the window size changed.
340 * the new size of the window
342 public void refresh(TerminalSize size
) {
346 if (getSize() == null || !getSize().equals(size
))
352 setActions(new ArrayList
<KeyAction
>(actions
), false);
358 public void invalidate() {
360 for (MainContent content
: contentStack
) {
361 content
.invalidate();
366 public boolean handleInput(KeyStroke key
) {
367 boolean handled
= false;
369 if (userQuestion
!= null) {
370 handled
= handleQuestion(userQuestion
, key
);
372 if (userQuestion
.getAnswer() != null) {
373 handleAction(userQuestion
.getAction(),
374 userQuestion
.getAnswer());
380 handled
= handleKey(key
);
384 handled
= super.handleInput(key
);
391 * Actually set the title <b>inside</b> the window. Will also call
392 * {@link BasicWindow#setTitle} with the computed parameters.
394 private void setTitle() {
395 String prefix
= " " + Main
.APPLICATION_TITLE
+ " (version "
396 + Main
.APPLICATION_VERSION
+ ")";
401 MainContent content
= getContent();
402 if (content
!= null) {
403 title
= content
.getTitle();
404 count
= content
.getCount();
410 if (title
.length() > 0) {
411 prefix
= prefix
+ ": ";
412 title
= StringUtils
.sanitize(title
, Main
.isUnicode());
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
= " " + Main
.trans(action
.getStringId()) + " ";
502 if (" ".equals(trans
))
505 String keyTrans
= KeyAction
.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();