2 * This file is part of lanterna (http://code.google.com/p/lanterna/).
4 * lanterna is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU Lesser General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU Lesser General Public License for more details.
14 * You should have received a copy of the GNU Lesser General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 * Copyright (C) 2010-2015 Martin
19 package com
.googlecode
.lanterna
.gui2
;
21 import com
.googlecode
.lanterna
.TerminalTextUtils
;
22 import com
.googlecode
.lanterna
.Symbols
;
23 import com
.googlecode
.lanterna
.TerminalPosition
;
24 import com
.googlecode
.lanterna
.TerminalSize
;
25 import com
.googlecode
.lanterna
.input
.KeyStroke
;
27 import java
.util
.ArrayList
;
28 import java
.util
.List
;
31 * Base class for several list box implementations, this will handle things like list of items and the scrollbar.
32 * @param <T> Should always be itself, see {@code AbstractComponent}
33 * @param <V> Type of items this list box contains
36 public abstract class AbstractListBox
<V
, T
extends AbstractListBox
<V
, T
>> extends AbstractInteractableComponent
<T
> {
37 private final List
<V
> items
;
38 private int selectedIndex
;
39 private ListItemRenderer
<V
,T
> listItemRenderer
;
42 * This constructor sets up the component so it has no preferred size but will ask to be as big as the list is. If
43 * the GUI cannot accommodate this size, scrolling and a vertical scrollbar will be used.
45 protected AbstractListBox() {
50 * This constructor sets up the component with a preferred size that is will always request, no matter what items
51 * are in the list box. If there are more items than the size can contain, scrolling and a vertical scrollbar will
52 * be used. Calling this constructor with a {@code null} value has the same effect as calling the default
55 * @param size Preferred size that the list should be asking for instead of invoking the preferred size calculation,
56 * or if set to {@code null} will ask to be big enough to display all items.
58 protected AbstractListBox(TerminalSize size
) {
59 this.items
= new ArrayList
<V
>();
60 this.selectedIndex
= -1;
61 setPreferredSize(size
);
62 setListItemRenderer(createDefaultListItemRenderer());
66 protected InteractableRenderer
<T
> createDefaultRenderer() {
67 return new DefaultListBoxRenderer
<V
, T
>();
71 * Method that constructs the {@code ListItemRenderer} that this list box should use to draw the elements of the
72 * list box. This can be overridden to supply a custom renderer. Note that this is not the renderer used for the
73 * entire list box but for each item, called one by one.
74 * @return {@code ListItemRenderer} to use when drawing the items in the list
76 protected ListItemRenderer
<V
,T
> createDefaultListItemRenderer() {
77 return new ListItemRenderer
<V
,T
>();
80 ListItemRenderer
<V
,T
> getListItemRenderer() {
81 return listItemRenderer
;
85 * This method overrides the {@code ListItemRenderer} that is used to draw each element in the list box. Note that
86 * this is not the renderer used for the entire list box but for each item, called one by one.
87 * @param listItemRenderer New renderer to use when drawing the items in the list box
90 public synchronized T
setListItemRenderer(ListItemRenderer
<V
,T
> listItemRenderer
) {
91 if(listItemRenderer
== null) {
92 listItemRenderer
= createDefaultListItemRenderer();
93 if(listItemRenderer
== null) {
94 throw new IllegalStateException("createDefaultListItemRenderer returned null");
97 this.listItemRenderer
= listItemRenderer
;
102 public synchronized Result
handleKeyStroke(KeyStroke keyStroke
) {
104 switch(keyStroke
.getKeyType()) {
106 return Result
.MOVE_FOCUS_NEXT
;
109 return Result
.MOVE_FOCUS_PREVIOUS
;
112 return Result
.MOVE_FOCUS_RIGHT
;
115 return Result
.MOVE_FOCUS_LEFT
;
118 if(items
.isEmpty() || selectedIndex
== items
.size() - 1) {
119 return Result
.MOVE_FOCUS_DOWN
;
122 return Result
.HANDLED
;
125 if(items
.isEmpty() || selectedIndex
== 0) {
126 return Result
.MOVE_FOCUS_UP
;
129 return Result
.HANDLED
;
133 return Result
.HANDLED
;
136 selectedIndex
= items
.size() - 1;
137 return Result
.HANDLED
;
140 if(getSize() != null) {
141 setSelectedIndex(getSelectedIndex() - getSize().getRows());
143 return Result
.HANDLED
;
146 if(getSize() != null) {
147 setSelectedIndex(getSelectedIndex() + getSize().getRows());
149 return Result
.HANDLED
;
153 return Result
.UNHANDLED
;
161 protected synchronized void afterEnterFocus(FocusChangeDirection direction
, Interactable previouslyInFocus
) {
162 if(items
.isEmpty()) {
166 if(direction
== FocusChangeDirection
.DOWN
) {
169 else if(direction
== FocusChangeDirection
.UP
) {
170 selectedIndex
= items
.size() - 1;
175 * Adds one more item to the list box, at the end.
176 * @param item Item to add to the list box
179 public synchronized T
addItem(V item
) {
185 if(selectedIndex
== -1) {
193 * Removes all items from the list box
196 public synchronized T
clearItems() {
204 * Looks for the particular item in the list and returns the index within the list (starting from zero) of that item
205 * if it is found, or -1 otherwise
206 * @param item What item to search for in the list box
207 * @return Index of the item in the list box or -1 if the list box does not contain the item
209 public synchronized int indexOf(V item
) {
210 return items
.indexOf(item
);
214 * Retrieves the item at the specified index in the list box
215 * @param index Index of the item to fetch
216 * @return The item at the specified index
217 * @throws IndexOutOfBoundsException If the index is less than zero or equals/greater than the number of items in
220 public synchronized V
getItemAt(int index
) {
221 return items
.get(index
);
225 * Checks if the list box has no items
226 * @return {@code true} if the list box has no items, {@code false} otherwise
228 public synchronized boolean isEmpty() {
229 return items
.isEmpty();
233 * Returns the number of items currently in the list box
234 * @return Number of items in the list box
236 public synchronized int getItemCount() {
241 * Returns a copy of the items in the list box as a {@code List}
242 * @return Copy of all the items in this list box
244 public synchronized List
<V
> getItems() {
245 return new ArrayList
<V
>(items
);
249 * Sets which item in the list box that is currently selected. Please note that in this context, selected simply
250 * means it is the item that currently has input focus. This is not to be confused with list box implementations
251 * such as {@code CheckBoxList} where individual items have a certain checked/unchecked state.
252 * @param index Index of the item that should be currently selected
255 public synchronized T
setSelectedIndex(int index
) {
256 selectedIndex
= index
;
257 if(selectedIndex
< 0) {
260 if(selectedIndex
> items
.size() - 1) {
261 selectedIndex
= items
.size() - 1;
268 * Returns the index of the currently selected item in the list box. Please note that in this context, selected
269 * simply means it is the item that currently has input focus. This is not to be confused with list box
270 * implementations such as {@code CheckBoxList} where individual items have a certain checked/unchecked state.
271 * @return The index of the currently selected row in the list box, or -1 if there are no items
273 public int getSelectedIndex() {
274 return selectedIndex
;
278 * Returns the currently selected item in the list box. Please note that in this context, selected
279 * simply means it is the item that currently has input focus. This is not to be confused with list box
280 * implementations such as {@code CheckBoxList} where individual items have a certain checked/unchecked state.
281 * @return The currently selected item in the list box, or {@code null} if there are no items
283 public synchronized V
getSelectedItem() {
284 if (selectedIndex
== -1) {
287 return items
.get(selectedIndex
);
292 * The default renderer for {@code AbstractListBox} and all its subclasses.
293 * @param <V> Type of the items the list box this renderer is for
294 * @param <T> Type of list box
296 public static class DefaultListBoxRenderer
<V
, T
extends AbstractListBox
<V
, T
>> implements InteractableRenderer
<T
> {
297 private int scrollTopIndex
;
300 * Default constructor
302 public DefaultListBoxRenderer() {
303 this.scrollTopIndex
= 0;
307 public TerminalPosition
getCursorLocation(T listBox
) {
308 int selectedIndex
= listBox
.getSelectedIndex();
309 int columnAccordingToRenderer
= listBox
.getListItemRenderer().getHotSpotPositionOnLine(selectedIndex
);
310 if(columnAccordingToRenderer
== -1) {
313 return new TerminalPosition(columnAccordingToRenderer
, selectedIndex
- scrollTopIndex
);
317 public TerminalSize
getPreferredSize(T listBox
) {
318 int maxWidth
= 5; //Set it to something...
320 for (V item
: listBox
.getItems()) {
321 String itemString
= listBox
.getListItemRenderer().getLabel(listBox
, index
++, item
);
322 int stringLengthInColumns
= TerminalTextUtils
.getColumnWidth(itemString
);
323 if (stringLengthInColumns
> maxWidth
) {
324 maxWidth
= stringLengthInColumns
;
327 return new TerminalSize(maxWidth
+ 1, listBox
.getItemCount());
331 public void drawComponent(TextGUIGraphics graphics
, T listBox
) {
332 //update the page size, used for page up and page down keys
333 int componentHeight
= graphics
.getSize().getRows();
334 int componentWidth
= graphics
.getSize().getColumns();
335 int selectedIndex
= listBox
.getSelectedIndex();
336 List
<V
> items
= listBox
.getItems();
337 ListItemRenderer
<V
,T
> listItemRenderer
= listBox
.getListItemRenderer();
339 if(selectedIndex
!= -1) {
340 if(selectedIndex
< scrollTopIndex
)
341 scrollTopIndex
= selectedIndex
;
342 else if(selectedIndex
>= componentHeight
+ scrollTopIndex
)
343 scrollTopIndex
= selectedIndex
- componentHeight
+ 1;
346 //Do we need to recalculate the scroll position?
347 //This code would be triggered by resizing the window when the scroll
348 //position is at the bottom
349 if(items
.size() > componentHeight
&&
350 items
.size() - scrollTopIndex
< componentHeight
) {
351 scrollTopIndex
= items
.size() - componentHeight
;
354 graphics
.applyThemeStyle(graphics
.getThemeDefinition(AbstractListBox
.class).getNormal());
357 TerminalSize itemSize
= graphics
.getSize().withRows(1);
358 for(int i
= scrollTopIndex
; i
< items
.size(); i
++) {
359 if(i
- scrollTopIndex
>= componentHeight
) {
362 listItemRenderer
.drawItem(
363 graphics
.newTextGraphics(new TerminalPosition(0, i
- scrollTopIndex
), itemSize
),
368 listBox
.isFocused());
371 graphics
.applyThemeStyle(graphics
.getThemeDefinition(AbstractListBox
.class).getNormal());
372 if(items
.size() > componentHeight
) {
373 graphics
.putString(componentWidth
- 1, 0, Symbols
.ARROW_UP
+ "");
375 graphics
.applyThemeStyle(graphics
.getThemeDefinition(AbstractListBox
.class).getInsensitive());
376 for(int i
= 1; i
< componentHeight
- 1; i
++)
377 graphics
.putString(componentWidth
- 1, i
, Symbols
.BLOCK_MIDDLE
+ "");
379 graphics
.applyThemeStyle(graphics
.getThemeDefinition(AbstractListBox
.class).getNormal());
380 graphics
.putString(componentWidth
- 1, componentHeight
- 1, Symbols
.ARROW_DOWN
+ "");
382 //Finally print the 'tick'
383 int scrollableSize
= items
.size() - componentHeight
;
384 double position
= (double)scrollTopIndex
/ ((double)scrollableSize
);
385 int tickPosition
= (int)(((double) componentHeight
- 3.0) * position
);
386 graphics
.applyThemeStyle(graphics
.getThemeDefinition(AbstractListBox
.class).getInsensitive());
387 graphics
.putString(componentWidth
- 1, 1 + tickPosition
, " ");
393 * The default list item renderer class, this can be extended and customized it needed. The instance which is
394 * assigned to the list box will be called once per item in the list when the list box is drawn.
395 * @param <V> Type of the items in the list box
396 * @param <T> Type of the list box class itself
398 public static class ListItemRenderer
<V
, T
extends AbstractListBox
<V
, T
>> {
400 * Returns where on the line to place the text terminal cursor for a currently selected item. By default this
401 * will return 0, meaning the first character of the selected line. If you extend {@code ListItemRenderer} you
402 * can change this by returning a different number. Returning -1 will cause lanterna to hide the cursor.
403 * @param selectedIndex Which item is currently selected
404 * @return Index of the character in the string we want to place the terminal cursor on, or -1 to hide it
406 public int getHotSpotPositionOnLine(int selectedIndex
) {
411 * Given a list box, an index of an item within that list box and what the item is, this method should return
412 * what to draw for that item. The default implementation is to return whatever {@code toString()} returns when
413 * called on the item.
414 * @param listBox List box the item belongs to
415 * @param index Index of the item
416 * @param item The item itself
417 * @return String to draw for this item
419 public String
getLabel(T listBox
, int index
, V item
) {
420 return item
!= null ? item
.toString() : "<null>";
424 * This is the main drawing method for a single list box item, it applies the current theme to setup the colors
425 * and then calls {@code getLabel(..)} and draws the result using the supplied {@code TextGUIGraphics}. The
426 * graphics object is created just for this item and is restricted so that it can only draw on the area this
427 * item is occupying. The top-left corner (0x0) should be the starting point when drawing the item.
428 * @param graphics Graphics object to draw with
429 * @param listBox List box we are drawing an item from
430 * @param index Index of the item we are drawing
431 * @param item The item we are drawing
432 * @param selected Will be set to {@code true} if the item is currently selected, otherwise {@code false}, but
433 * please notice what context 'selected' refers to here (see {@code setSelectedIndex})
434 * @param focused Will be set to {@code true} if the list box currently has input focus, otherwise {@code false}
436 public void drawItem(TextGUIGraphics graphics
, T listBox
, int index
, V item
, boolean selected
, boolean focused
) {
437 if(selected
&& focused
) {
438 graphics
.applyThemeStyle(graphics
.getThemeDefinition(AbstractListBox
.class).getSelected());
441 graphics
.applyThemeStyle(graphics
.getThemeDefinition(AbstractListBox
.class).getNormal());
443 String label
= getLabel(listBox
, index
, item
);
444 label
= TerminalTextUtils
.fitString(label
, graphics
.getSize().getColumns());
445 graphics
.putString(0, 0, label
);