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
.Card
;
10 import be
.nikiroo
.jvcard
.Contact
;
11 import be
.nikiroo
.jvcard
.Data
;
12 import be
.nikiroo
.jvcard
.i18n
.Trans
;
13 import be
.nikiroo
.jvcard
.i18n
.Trans
.StringId
;
14 import be
.nikiroo
.jvcard
.tui
.KeyAction
.Mode
;
15 import be
.nikiroo
.jvcard
.tui
.UiColors
.Element
;
16 import be
.nikiroo
.jvcard
.tui
.panes
.ContactDetails
;
17 import be
.nikiroo
.jvcard
.tui
.panes
.ContactDetailsRaw
;
18 import be
.nikiroo
.jvcard
.tui
.panes
.ContactList
;
19 import be
.nikiroo
.jvcard
.tui
.panes
.MainContent
;
21 import com
.googlecode
.lanterna
.TerminalSize
;
22 import com
.googlecode
.lanterna
.gui2
.BasicWindow
;
23 import com
.googlecode
.lanterna
.gui2
.BorderLayout
;
24 import com
.googlecode
.lanterna
.gui2
.Direction
;
25 import com
.googlecode
.lanterna
.gui2
.Interactable
;
26 import com
.googlecode
.lanterna
.gui2
.Label
;
27 import com
.googlecode
.lanterna
.gui2
.LinearLayout
;
28 import com
.googlecode
.lanterna
.gui2
.Panel
;
29 import com
.googlecode
.lanterna
.gui2
.TextBox
;
30 import com
.googlecode
.lanterna
.gui2
.Window
;
31 import com
.googlecode
.lanterna
.input
.KeyStroke
;
32 import com
.googlecode
.lanterna
.input
.KeyType
;
35 * This is the main "window" of the program. It can show up to one
36 * {@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 boolean waitForOneKeyAnswer
;
46 private KeyAction questionAction
;
47 private String titleCache
;
48 private Panel titlePanel
;
49 private Panel mainPanel
;
50 private Panel contentPanel
;
51 private Panel actionPanel
;
52 private Panel messagePanel
;
56 * Create a new, empty window.
63 * Create a new window hosting the given content.
68 public MainWindow(MainContent content
) {
69 super(content
== null ?
"" : content
.getTitle());
71 setHints(Arrays
.asList(Window
.Hint
.FULL_SCREEN
,
72 Window
.Hint
.NO_DECORATIONS
, Window
.Hint
.FIT_TERMINAL_WINDOW
));
74 defaultActions
.add(new KeyAction(Mode
.BACK
, 'q',
75 StringId
.KEY_ACTION_BACK
));
76 defaultActions
.add(new KeyAction(Mode
.BACK
, KeyType
.Escape
,
78 defaultActions
.add(new KeyAction(Mode
.HELP
, 'h',
79 StringId
.KEY_ACTION_HELP
));
80 defaultActions
.add(new KeyAction(Mode
.HELP
, KeyType
.F1
, StringId
.NULL
));
82 actionPanel
= new Panel();
83 contentPanel
= new Panel();
84 mainPanel
= new Panel();
85 messagePanel
= new Panel();
86 titlePanel
= new Panel();
88 Panel actionMessagePanel
= new Panel();
90 LinearLayout llayout
= new LinearLayout(Direction
.HORIZONTAL
);
91 llayout
.setSpacing(0);
92 actionPanel
.setLayoutManager(llayout
);
94 BorderLayout blayout
= new BorderLayout();
95 titlePanel
.setLayoutManager(blayout
);
97 llayout
= new LinearLayout(Direction
.VERTICAL
);
98 llayout
.setSpacing(0);
99 messagePanel
.setLayoutManager(llayout
);
101 blayout
= new BorderLayout();
102 mainPanel
.setLayoutManager(blayout
);
104 blayout
= new BorderLayout();
105 contentPanel
.setLayoutManager(blayout
);
107 blayout
= new BorderLayout();
108 actionMessagePanel
.setLayoutManager(blayout
);
111 .addComponent(messagePanel
, BorderLayout
.Location
.TOP
);
112 actionMessagePanel
.addComponent(actionPanel
,
113 BorderLayout
.Location
.CENTER
);
115 mainPanel
.addComponent(titlePanel
, BorderLayout
.Location
.TOP
);
116 mainPanel
.addComponent(contentPanel
, BorderLayout
.Location
.CENTER
);
118 .addComponent(actionMessagePanel
, BorderLayout
.Location
.BOTTOM
);
120 pushContent(content
);
122 setComponent(mainPanel
);
126 * "push" some content to the window stack.
129 * the new top-of-the-stack content
131 public void pushContent(MainContent content
) {
132 List
<KeyAction
> actions
= null;
134 contentPanel
.removeAllComponents();
135 if (content
!= null) {
136 actions
= content
.getKeyBindings();
137 contentPanel
.addComponent(content
, BorderLayout
.Location
.CENTER
);
138 this.contentStack
.add(content
);
140 Interactable focus
= content
.nextFocus(null);
146 setActions(actions
, true);
150 * "pop" the latest, top-of-the-stack content from the window stack.
152 * @return the removed content if any
154 public MainContent
popContent() {
155 MainContent removed
= null;
156 MainContent prev
= null;
158 MainContent content
= getContent();
160 removed
= contentStack
.remove(contentStack
.size() - 1);
162 if (contentStack
.size() > 0)
163 prev
= contentStack
.remove(contentStack
.size() - 1);
171 * Show the given message on screen. It will disappear at the next action.
174 * the message to display
176 * TRUE for an error message, FALSE for an information message
178 * @return TRUE if changes were performed
180 public boolean setMessage(String mess
, boolean error
) {
181 if (mess
!= null || messagePanel
.getChildCount() > 0) {
182 messagePanel
.removeAllComponents();
184 Element element
= (error ? UiColors
.Element
.LINE_MESSAGE_ERR
185 : UiColors
.Element
.LINE_MESSAGE
);
186 Label lbl
= element
.createLabel(" " + mess
+ " ");
187 messagePanel
.addComponent(lbl
, LinearLayout
188 .createLayoutData(LinearLayout
.Alignment
.Center
));
197 * Show a question to the user and switch to "ask for answer" mode see
198 * {@link MainWindow#handleQuestion}. The user will be asked to enter some
199 * answer and confirm with ENTER.
204 * the question to ask
206 * the initial answer if any (to be edited by the user)
208 public void setQuestion(KeyAction action
, String question
, String initial
) {
209 setQuestion(action
, question
, initial
, false);
213 * Show a question to the user and switch to "ask for answer" mode see
214 * {@link MainWindow#handleQuestion}. The user will be asked to hit one key
220 * the question to ask
222 public void setQuestion(KeyAction action
, String question
) {
223 setQuestion(action
, question
, null, true);
227 * Show a question to the user and switch to "ask for answer" mode see
228 * {@link MainWindow#handleQuestion}.
233 * the question to ask
235 * the initial answer if any (to be edited by the user)
237 * TRUE for a one-key answer, FALSE for a text answer validated
240 private void setQuestion(KeyAction action
, String question
, String initial
,
242 questionAction
= action
;
243 waitForOneKeyAnswer
= oneKey
;
245 messagePanel
.removeAllComponents();
247 Panel hpanel
= new Panel();
248 LinearLayout llayout
= new LinearLayout(Direction
.HORIZONTAL
);
249 llayout
.setSpacing(0);
250 hpanel
.setLayoutManager(llayout
);
252 Label lbl
= UiColors
.Element
.LINE_MESSAGE_QUESTION
.createLabel(" "
254 text
= new TextBox(new TerminalSize(getSize().getColumns()
255 - lbl
.getSize().getColumns(), 1));
257 text
.setText(initial
);
259 hpanel
.addComponent(lbl
,
260 LinearLayout
.createLayoutData(LinearLayout
.Alignment
.Beginning
));
261 hpanel
.addComponent(text
,
262 LinearLayout
.createLayoutData(LinearLayout
.Alignment
.Fill
));
265 .addComponent(hpanel
, LinearLayout
266 .createLayoutData(LinearLayout
.Alignment
.Beginning
));
272 * Refresh the window and the empty-space handling. You should call this
273 * method when the window size changed.
276 * the new size of the window
278 public void refresh(TerminalSize size
) {
282 if (getSize() == null || !getSize().equals(size
))
288 setActions(new ArrayList
<KeyAction
>(actions
), false);
294 public void invalidate() {
296 for (MainContent content
: contentStack
) {
297 content
.invalidate();
302 public boolean handleInput(KeyStroke key
) {
303 boolean handled
= false;
305 if (questionAction
!= null) {
306 String answer
= handleQuestion(key
);
307 if (answer
!= null) {
310 handleAction(questionAction
, answer
);
311 questionAction
= null;
314 handled
= handleKey(key
);
318 handled
= super.handleInput(key
);
324 * Actually set the title <b>inside</b> the window. Will also call
325 * {@link BasicWindow#setTitle} with the computed parameters.
327 private void setTitle() {
328 String prefix
= " " + Main
.APPLICATION_TITLE
+ " (version "
329 + Main
.APPLICATION_VERSION
+ ")";
334 MainContent content
= getContent();
335 if (content
!= null) {
336 title
= content
.getTitle();
337 count
= content
.getCount();
343 if (title
.length() > 0) {
344 prefix
= prefix
+ ": ";
345 title
= StringUtils
.sanitize(title
, UiColors
.getInstance()
349 String countStr
= "";
351 countStr
= "[" + count
+ "]";
355 if (getSize() != null) {
356 width
= getSize().getColumns();
360 int padding
= width
- prefix
.length() - title
.length()
363 if (title
.length() > 0)
364 title
= StringUtils
.padString(title
, title
.length()
367 prefix
= StringUtils
.padString(prefix
, prefix
.length()
372 String titleCache
= prefix
+ title
+ count
;
373 if (!titleCache
.equals(this.titleCache
)) {
374 super.setTitle(prefix
);
376 Label lblPrefix
= new Label(prefix
);
377 UiColors
.Element
.TITLE_MAIN
.themeLabel(lblPrefix
);
379 Label lblTitle
= null;
380 if (title
.length() > 0) {
381 lblTitle
= new Label(title
);
382 UiColors
.Element
.TITLE_VARIABLE
.themeLabel(lblTitle
);
385 Label lblCount
= null;
386 if (countStr
!= null) {
387 lblCount
= new Label(countStr
);
388 UiColors
.Element
.TITLE_COUNT
.themeLabel(lblCount
);
391 titlePanel
.removeAllComponents();
393 titlePanel
.addComponent(lblPrefix
, BorderLayout
.Location
.LEFT
);
394 if (lblTitle
!= null)
395 titlePanel
.addComponent(lblTitle
, BorderLayout
.Location
.CENTER
);
396 if (lblCount
!= null)
397 titlePanel
.addComponent(lblCount
, BorderLayout
.Location
.RIGHT
);
402 * Return the current {@link MainContent} from the stack if any.
404 * @return the current {@link MainContent}
406 private MainContent
getContent() {
407 if (contentStack
.size() > 0) {
408 return contentStack
.get(contentStack
.size() - 1);
415 * Update the list of actions and refresh the action panel.
418 * the list of actions to support
419 * @param enableDefaultactions
420 * TRUE to enable the default actions
422 private void setActions(List
<KeyAction
> actions
,
423 boolean enableDefaultactions
) {
424 this.actions
.clear();
426 if (enableDefaultactions
)
427 this.actions
.addAll(defaultActions
);
430 this.actions
.addAll(actions
);
432 actionPanel
.removeAllComponents();
433 for (KeyAction action
: this.actions
) {
434 String trans
= " " + action
.getStringId().trans() + " ";
436 if (" ".equals(trans
))
439 String keyTrans
= Trans
.getInstance().trans(action
.getKey());
441 Panel kPane
= new Panel();
442 LinearLayout layout
= new LinearLayout(Direction
.HORIZONTAL
);
443 layout
.setSpacing(0);
444 kPane
.setLayoutManager(layout
);
446 kPane
.addComponent(UiColors
.Element
.ACTION_KEY
447 .createLabel(keyTrans
));
448 kPane
.addComponent(UiColors
.Element
.ACTION_DESC
.createLabel(trans
));
450 actionPanel
.addComponent(kPane
);
453 // fill with "desc" colour
455 if (getSize() != null) {
456 width
= getSize().getColumns();
460 actionPanel
.addComponent(UiColors
.Element
.ACTION_DESC
461 .createLabel(StringUtils
.padString("", width
)));
466 * Handle user input when in "ask for question" mode (see
467 * {@link MainWindow#questionKey}).
470 * the key that has been pressed by the user
472 * @return the user's answer if done
474 private String
handleQuestion(KeyStroke key
) {
475 String answer
= null;
477 if (waitForOneKeyAnswer
) {
478 answer
= "" + key
.getCharacter();
480 if (key
.getKeyType() == KeyType
.Enter
) {
482 answer
= text
.getText();
488 if (answer
!= null) {
489 Interactable focus
= null;
490 MainContent content
= getContent();
492 focus
= content
.nextFocus(null);
501 * Handle the input in case of "normal" (not "ask for answer") mode.
504 * the key that was pressed
506 * the answer given for this key
508 * @return if the window handled the input
510 private boolean handleKey(KeyStroke key
) {
511 boolean handled
= false;
513 if (setMessage(null, false))
516 for (KeyAction action
: actions
) {
517 if (!action
.match(key
))
522 if (action
.onAction()) {
523 handleAction(action
, null);
533 * Handle the input in case of "normal" (not "ask for answer") mode.
536 * the key that was pressed
538 * the answer given for this key
540 * @return if the window handled the input
542 private void handleAction(KeyAction action
, String answer
) {
543 MainContent content
= getContent();
545 Card card
= action
.getCard();
546 Contact contact
= action
.getContact();
547 Data data
= action
.getData();
549 switch (action
.getMode()) {
554 if (action
.getKey().getKeyType() == KeyType
.ArrowUp
)
556 if (action
.getKey().getKeyType() == KeyType
.ArrowDown
)
558 if (action
.getKey().getKeyType() == KeyType
.ArrowLeft
)
560 if (action
.getKey().getKeyType() == KeyType
.ArrowRight
)
563 if (content
!= null) {
564 String err
= content
.move(x
, y
);
566 setMessage(err
, true);
570 // mode with windows:
573 pushContent(new ContactList(card
));
576 case CONTACT_DETAILS
:
577 if (contact
!= null) {
578 pushContent(new ContactDetails(contact
));
581 case CONTACT_DETAILS_RAW
:
582 if (contact
!= null) {
583 pushContent(new ContactDetailsRaw(contact
));
586 // mode interpreted by MainWindow:
589 // setMessage("Help! I need somebody! Help!", false);
590 if (answer
== null) {
591 setQuestion(action
, "Test question?", "[initial]");
593 setMessage("You answered: " + answer
, false);
598 String warning
= content
.getExitWarning();
599 if (warning
!= null) {
600 if (answer
== null) {
601 setQuestion(action
, warning
);
603 setMessage(null, false);
604 if (answer
.equalsIgnoreCase("y")) {
612 if (contentStack
.size() == 0) {
619 if (answer
== null) {
621 String name
= data
.getName();
622 String value
= data
.getValue();
623 setQuestion(action
, name
, value
);
626 setMessage(null, false);
627 data
.setValue(answer
);
631 if (answer
== null) {
632 if (contact
!= null) {
633 setQuestion(action
, "Delete contact? [Y/N]");
636 setMessage(null, false);
637 if (answer
.equalsIgnoreCase("y")) {
638 if (contact
.delete()) {
639 content
.refreshData();
643 setMessage("Cannot delete this contact", true);
649 if (answer
== null) {
651 setQuestion(action
, "Save changes? [Y/N]");
654 setMessage(null, false);
655 if (answer
.equalsIgnoreCase("y")) {
662 } catch (IOException ioe
) {
663 ioe
.printStackTrace();
667 setMessage("Cannot save to file", true);