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
.ColorOption
;
11 import be
.nikiroo
.jvcard
.resources
.StringId
;
12 import be
.nikiroo
.jvcard
.tui
.KeyAction
.Mode
;
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
;
17 import be
.nikiroo
.utils
.StringUtils
;
18 import be
.nikiroo
.utils
.Version
;
20 import com
.googlecode
.lanterna
.TerminalSize
;
21 import com
.googlecode
.lanterna
.gui2
.BasicWindow
;
22 import com
.googlecode
.lanterna
.gui2
.BorderLayout
;
23 import com
.googlecode
.lanterna
.gui2
.Direction
;
24 import com
.googlecode
.lanterna
.gui2
.Interactable
;
25 import com
.googlecode
.lanterna
.gui2
.Label
;
26 import com
.googlecode
.lanterna
.gui2
.LinearLayout
;
27 import com
.googlecode
.lanterna
.gui2
.Panel
;
28 import com
.googlecode
.lanterna
.gui2
.TextBox
;
29 import com
.googlecode
.lanterna
.gui2
.Window
;
30 import com
.googlecode
.lanterna
.input
.KeyStroke
;
31 import com
.googlecode
.lanterna
.input
.KeyType
;
34 * This is the main "window" of the program. It can show up to one
35 * {@link MainContent} at any one time, but keeps a stack of contents.
41 public class MainWindow
extends BasicWindow
{
42 private List
<KeyAction
> defaultActions
= new LinkedList
<KeyAction
>();
43 private List
<KeyAction
> actions
= new LinkedList
<KeyAction
>();
44 private List
<MainContent
> contentStack
= new LinkedList
<MainContent
>();
45 private UserQuestion userQuestion
;
46 private String titleCache
;
47 private Panel titlePanel
;
48 private Panel mainPanel
;
49 private Panel contentPanel
;
50 private Panel actionPanel
;
51 private Panel messagePanel
;
55 * Information about a question to ask the user and its answer.
60 private class UserQuestion
{
61 private boolean oneKeyAnswer
;
62 private KeyAction action
;
63 private String answer
;
66 * Create a new {@link UserQuestion}.
69 * the action that triggered the question
71 * TRUE if we expect a one-key answer
73 public UserQuestion(KeyAction action
, boolean oneKeyAnswer
) {
75 this.oneKeyAnswer
= oneKeyAnswer
;
79 * Return the {@link KeyAction} that triggered the question.
81 * @return the {@link KeyAction}
83 public KeyAction
getAction() {
88 * Check if a one-key answer is expected.
90 * @return TRUE if a one-key answer is expected
92 public boolean isOneKeyAnswer() {
97 * Return the user answer.
99 * @return the user answer
101 public String
getAnswer() {
106 * Set the user answer.
111 public void setAnswer(String answer
) {
112 this.answer
= answer
;
117 * Create a new, empty window.
119 public MainWindow() {
124 * Create a new window hosting the given content.
127 * the content to host
129 public MainWindow(MainContent content
) {
130 super(content
== null ?
"" : content
.getTitle());
132 setHints(Arrays
.asList(Window
.Hint
.FULL_SCREEN
,
133 Window
.Hint
.NO_DECORATIONS
, Window
.Hint
.FIT_TERMINAL_WINDOW
));
135 defaultActions
.add(new KeyAction(Mode
.BACK
, 'q',
136 StringId
.KEY_ACTION_BACK
));
137 defaultActions
.add(new KeyAction(Mode
.BACK
, KeyType
.Escape
,
139 defaultActions
.add(new KeyAction(Mode
.HELP
, 'h',
140 StringId
.KEY_ACTION_HELP
));
141 defaultActions
.add(new KeyAction(Mode
.HELP
, KeyType
.F1
, StringId
.NULL
));
143 actionPanel
= new Panel();
144 contentPanel
= new Panel();
145 mainPanel
= new Panel();
146 messagePanel
= new Panel();
147 titlePanel
= new Panel();
149 Panel actionMessagePanel
= new Panel();
151 LinearLayout llayout
= new LinearLayout(Direction
.HORIZONTAL
);
152 llayout
.setSpacing(0);
153 actionPanel
.setLayoutManager(llayout
);
155 BorderLayout blayout
= new BorderLayout();
156 titlePanel
.setLayoutManager(blayout
);
158 llayout
= new LinearLayout(Direction
.VERTICAL
);
159 llayout
.setSpacing(0);
160 messagePanel
.setLayoutManager(llayout
);
162 blayout
= new BorderLayout();
163 mainPanel
.setLayoutManager(blayout
);
165 blayout
= new BorderLayout();
166 contentPanel
.setLayoutManager(blayout
);
168 blayout
= new BorderLayout();
169 actionMessagePanel
.setLayoutManager(blayout
);
172 .addComponent(messagePanel
, BorderLayout
.Location
.TOP
);
173 actionMessagePanel
.addComponent(actionPanel
,
174 BorderLayout
.Location
.CENTER
);
176 mainPanel
.addComponent(titlePanel
, BorderLayout
.Location
.TOP
);
177 mainPanel
.addComponent(contentPanel
, BorderLayout
.Location
.CENTER
);
179 .addComponent(actionMessagePanel
, BorderLayout
.Location
.BOTTOM
);
181 pushContent(content
);
183 setComponent(mainPanel
);
187 * "push" some content to the window stack.
190 * the new top-of-the-stack content
192 public void pushContent(MainContent content
) {
193 List
<KeyAction
> actions
= null;
195 contentPanel
.removeAllComponents();
196 if (content
!= null) {
197 actions
= content
.getKeyBindings();
198 contentPanel
.addComponent(content
, BorderLayout
.Location
.CENTER
);
199 this.contentStack
.add(content
);
201 Interactable focus
= content
.nextFocus(null);
207 setActions(actions
, true);
211 * "pop" the latest, top-of-the-stack content from the window stack.
213 * @return the removed content if any
215 public MainContent
popContent() {
216 MainContent removed
= null;
217 MainContent prev
= null;
219 MainContent content
= getContent();
221 removed
= contentStack
.remove(contentStack
.size() - 1);
223 if (contentStack
.size() > 0)
224 prev
= contentStack
.remove(contentStack
.size() - 1);
228 String mess
= prev
.wakeup();
230 setMessage(mess
, false);
231 } catch (IOException e
) {
232 setMessage(e
.getMessage(), true);
242 * Show the given message on screen. It will disappear at the next action.
245 * the message to display
247 * TRUE for an error message, FALSE for an information message
249 * @return TRUE if changes were performed
251 public boolean setMessage(String mess
, boolean error
) {
252 if (mess
!= null || messagePanel
.getChildCount() > 0) {
253 messagePanel
.removeAllComponents();
255 ColorOption element
= (error ? ColorOption
.LINE_MESSAGE_ERR
256 : ColorOption
.LINE_MESSAGE
);
257 Label lbl
= UiColors
.createLabel(element
, " " + mess
+ " ");
258 messagePanel
.addComponent(lbl
, LinearLayout
259 .createLayoutData(LinearLayout
.Alignment
.Center
));
268 * Show a question to the user and switch to "ask for answer" mode see
269 * {@link MainWindow#handleQuestion}. The user will be asked to enter some
270 * answer and confirm with ENTER.
275 * the question to ask
277 * the initial answer if any (to be edited by the user)
279 public void setQuestion(KeyAction action
, String question
, String initial
) {
280 setQuestion(action
, question
, initial
, false);
284 * Show a question to the user and switch to "ask for answer" mode see
285 * {@link MainWindow#handleQuestion}. The user will be asked to hit one key
291 * the question to ask
293 public void setQuestion(KeyAction action
, String question
) {
294 setQuestion(action
, question
, null, true);
298 * Show a question to the user and switch to "ask for answer" mode see
299 * {@link MainWindow#handleQuestion}.
304 * the question to ask
306 * the initial answer if any (to be edited by the user)
308 * TRUE for a one-key answer, FALSE for a text answer validated
311 private void setQuestion(KeyAction action
, String question
, String initial
,
313 userQuestion
= new UserQuestion(action
, oneKey
);
315 messagePanel
.removeAllComponents();
317 Panel hpanel
= new Panel();
318 LinearLayout llayout
= new LinearLayout(Direction
.HORIZONTAL
);
319 llayout
.setSpacing(0);
320 hpanel
.setLayoutManager(llayout
);
322 Label lbl
= UiColors
.createLabel(ColorOption
.LINE_MESSAGE_QUESTION
, " "
324 text
= new TextBox(new TerminalSize(getSize().getColumns()
325 - lbl
.getSize().getColumns(), 1));
327 if (initial
!= null) {
328 // add all chars one by one so the caret is at the end
329 for (int index
= 0; index
< initial
.length(); index
++) {
330 text
.handleInput(new KeyStroke(initial
.charAt(index
), false,
335 hpanel
.addComponent(lbl
,
336 LinearLayout
.createLayoutData(LinearLayout
.Alignment
.Beginning
));
337 hpanel
.addComponent(text
,
338 LinearLayout
.createLayoutData(LinearLayout
.Alignment
.Fill
));
341 .addComponent(hpanel
, LinearLayout
342 .createLayoutData(LinearLayout
.Alignment
.Beginning
));
348 * Refresh the window and the empty-space handling. You should call this
349 * method when the window size changed.
352 * the new size of the window
354 public void refresh(TerminalSize size
) {
358 if (getSize() == null || !getSize().equals(size
))
364 setActions(new ArrayList
<KeyAction
>(actions
), false);
370 public void invalidate() {
372 for (MainContent content
: contentStack
) {
373 content
.invalidate();
378 public boolean handleInput(KeyStroke key
) {
379 boolean handled
= false;
381 if (userQuestion
!= null) {
382 handled
= handleQuestion(userQuestion
, key
);
384 if (userQuestion
.getAnswer() != null) {
385 handleAction(userQuestion
.getAction(),
386 userQuestion
.getAnswer());
392 handled
= handleKey(key
);
396 handled
= super.handleInput(key
);
403 * Actually set the title <b>inside</b> the window. Will also call
404 * {@link BasicWindow#setTitle} with the computed parameters.
406 private void setTitle() {
407 String prefix
= " " + Main
.APPLICATION_TITLE
+ " (version "
408 + Version
.getCurrentVersion() + ")";
413 MainContent content
= getContent();
414 if (content
!= null) {
415 title
= content
.getTitle();
416 count
= content
.getCount();
422 if (title
.length() > 0) {
423 prefix
= prefix
+ ": ";
424 title
= StringUtils
.sanitize(title
, Main
.isUnicode());
427 String countStr
= "";
429 countStr
= "[" + count
+ "]";
433 if (getSize() != null) {
434 width
= getSize().getColumns();
438 int padding
= width
- prefix
.length() - title
.length()
441 if (title
.length() > 0)
442 title
= StringUtils
.padString(title
, title
.length()
445 prefix
= StringUtils
.padString(prefix
, prefix
.length()
450 String titleCache
= prefix
+ title
+ count
;
451 if (!titleCache
.equals(this.titleCache
)) {
452 super.setTitle(prefix
);
454 Label lblPrefix
= new Label(prefix
);
455 UiColors
.themeLabel(ColorOption
.TITLE_MAIN
, lblPrefix
);
457 Label lblTitle
= null;
458 if (title
.length() > 0) {
459 lblTitle
= new Label(title
);
460 UiColors
.themeLabel(ColorOption
.TITLE_VARIABLE
, lblTitle
);
463 Label lblCount
= new Label(countStr
);
464 UiColors
.themeLabel(ColorOption
.TITLE_COUNT
, lblCount
);
466 titlePanel
.removeAllComponents();
468 titlePanel
.addComponent(lblPrefix
, BorderLayout
.Location
.LEFT
);
469 if (lblTitle
!= null) {
470 titlePanel
.addComponent(lblTitle
, BorderLayout
.Location
.CENTER
);
472 titlePanel
.addComponent(lblCount
, BorderLayout
.Location
.RIGHT
);
477 * Return the current {@link MainContent} from the stack if any.
479 * @return the current {@link MainContent}
481 private MainContent
getContent() {
482 if (contentStack
.size() > 0) {
483 return contentStack
.get(contentStack
.size() - 1);
490 * Update the list of actions and refresh the action panel.
493 * the list of actions to support
494 * @param enableDefaultactions
495 * TRUE to enable the default actions
497 private void setActions(List
<KeyAction
> actions
,
498 boolean enableDefaultactions
) {
499 this.actions
.clear();
501 if (enableDefaultactions
)
502 this.actions
.addAll(defaultActions
);
505 this.actions
.addAll(actions
);
507 actionPanel
.removeAllComponents();
508 for (KeyAction action
: this.actions
) {
509 String trans
= " " + Main
.trans(action
.getStringId()) + " ";
511 if (" ".equals(trans
))
514 String keyTrans
= KeyAction
.trans(action
.getKey());
516 Panel kPane
= new Panel();
517 LinearLayout layout
= new LinearLayout(Direction
.HORIZONTAL
);
518 layout
.setSpacing(0);
519 kPane
.setLayoutManager(layout
);
521 kPane
.addComponent(UiColors
.createLabel(ColorOption
.ACTION_KEY
,
523 kPane
.addComponent(UiColors
.createLabel(ColorOption
.ACTION_DESC
,
526 actionPanel
.addComponent(kPane
);
529 // fill with "desc" colour
531 if (getSize() != null) {
532 width
= getSize().getColumns();
536 actionPanel
.addComponent(UiColors
.createLabel(
537 ColorOption
.ACTION_DESC
, StringUtils
.padString("", width
)));
542 * Handle user input when in "ask for question" mode (see
543 * {@link MainWindow#userQuestion}).
545 * @param userQuestion
548 * the key that has been pressed by the user
550 * @return TRUE if the {@link KeyStroke} was handled
552 private boolean handleQuestion(UserQuestion userQuestion
, KeyStroke key
) {
553 userQuestion
.setAnswer(null);
555 if (userQuestion
.isOneKeyAnswer()) {
556 userQuestion
.setAnswer("" + key
.getCharacter());
559 if (key
.isCtrlDown() && key
.getCharacter() == 'h') {
560 key
= new KeyStroke(KeyType
.Backspace
);
563 switch (key
.getKeyType()) {
566 userQuestion
.setAnswer(text
.getText());
568 userQuestion
.setAnswer("");
571 int pos
= text
.getCaretPosition().getColumn();
573 String current
= text
.getText();
574 // force caret one space before:
575 text
.setText(current
.substring(0, pos
- 1));
577 text
.setText(current
.substring(0, pos
- 1)
578 + current
.substring(pos
));
582 // Do nothing (continue entering text)
587 if (userQuestion
.getAnswer() != null) {
588 Interactable focus
= null;
589 MainContent content
= getContent();
591 focus
= content
.nextFocus(null);
602 * Handle the input in case of "normal" (not "ask for answer") mode.
605 * the key that was pressed
607 * @return if the window handled the input
609 private boolean handleKey(KeyStroke key
) {
610 boolean handled
= false;
612 if (setMessage(null, false))
615 for (KeyAction action
: actions
) {
616 if (!action
.match(key
))
621 action
.getObject(); // see {@link KeyAction#getMessage()}
622 String mess
= action
.getMessage();
624 setMessage(mess
, action
.isError());
627 if (!action
.isError() && action
.onAction()) {
628 handleAction(action
, null);
638 * Handle the input in case of "normal" (not "ask for answer") mode.
641 * the key that was pressed and the action to take
643 * the answer given for this key
646 private void handleAction(KeyAction action
, String answer
) {
647 MainContent content
= getContent();
649 switch (action
.getMode()) {
654 if (action
.getKey().getKeyType() == KeyType
.ArrowUp
)
656 if (action
.getKey().getKeyType() == KeyType
.ArrowDown
)
658 if (action
.getKey().getKeyType() == KeyType
.ArrowLeft
)
660 if (action
.getKey().getKeyType() == KeyType
.ArrowRight
)
663 if (content
!= null) {
664 String err
= content
.move(x
, y
);
666 setMessage(err
, true);
670 // mode with windows:
672 if (action
.getCard() != null) {
673 pushContent(new ContactList(action
.getCard()));
674 } else if (action
.getObject() != null
675 && action
.getObject() instanceof MainContent
) {
676 MainContent mergeContent
= (MainContent
) action
.getObject();
677 pushContent(mergeContent
);
680 case CONTACT_DETAILS
:
681 if (action
.getContact() != null) {
682 pushContent(new ContactDetails(action
.getContact()));
685 case CONTACT_DETAILS_RAW
:
686 if (action
.getContact() != null) {
687 pushContent(new ContactDetailsRaw(action
.getContact()));
690 // mode interpreted by MainWindow:
693 setMessage("Help! I need somebody! Help!", false);
697 String warning
= content
.getExitWarning();
698 if (warning
!= null) {
699 if (answer
== null) {
700 setQuestion(action
, warning
);
702 setMessage(null, false);
703 if (answer
.equalsIgnoreCase("y")) {
711 if (contentStack
.size() == 0) {
718 if (answer
== null) {
719 setQuestion(action
, action
.getQuestion(),
720 action
.getDefaultAnswer());
722 setMessage(action
.callback(answer
), true);
723 content
.refreshData();
729 if (answer
== null) {
730 setQuestion(action
, action
.getQuestion());
732 setMessage(action
.callback(answer
), true);
733 content
.refreshData();