Change build scripts
[jvcard.git] / src / com / googlecode / lanterna / gui2 / AbstractListBox.java
CommitLineData
a3b510ab
NR
1/*
2 * This file is part of lanterna (http://code.google.com/p/lanterna/).
3 *
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.
8 *
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.
13 *
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/>.
16 *
17 * Copyright (C) 2010-2015 Martin
18 */
19package com.googlecode.lanterna.gui2;
20
21import com.googlecode.lanterna.TerminalTextUtils;
22import com.googlecode.lanterna.Symbols;
23import com.googlecode.lanterna.TerminalPosition;
24import com.googlecode.lanterna.TerminalSize;
25import com.googlecode.lanterna.input.KeyStroke;
26
27import java.util.ArrayList;
28import java.util.List;
29
30/**
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
34 * @author Martin
35 */
36public 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;
40
41 /**
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.
44 */
45 protected AbstractListBox() {
46 this(null);
47 }
48
49 /**
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
53 * constructor.
54 *
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.
57 */
58 protected AbstractListBox(TerminalSize size) {
59 this.items = new ArrayList<V>();
60 this.selectedIndex = -1;
61 setPreferredSize(size);
62 setListItemRenderer(createDefaultListItemRenderer());
63 }
64
65 @Override
66 protected InteractableRenderer<T> createDefaultRenderer() {
67 return new DefaultListBoxRenderer<V, T>();
68 }
69
70 /**
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
75 */
76 protected ListItemRenderer<V,T> createDefaultListItemRenderer() {
77 return new ListItemRenderer<V,T>();
78 }
79
80 ListItemRenderer<V,T> getListItemRenderer() {
81 return listItemRenderer;
82 }
83
84 /**
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
88 * @return Itself
89 */
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");
95 }
96 }
97 this.listItemRenderer = listItemRenderer;
98 return self();
99 }
100
101 @Override
102 public synchronized Result handleKeyStroke(KeyStroke keyStroke) {
103 try {
104 switch(keyStroke.getKeyType()) {
105 case Tab:
106 return Result.MOVE_FOCUS_NEXT;
107
108 case ReverseTab:
109 return Result.MOVE_FOCUS_PREVIOUS;
110
111 case ArrowRight:
112 return Result.MOVE_FOCUS_RIGHT;
113
114 case ArrowLeft:
115 return Result.MOVE_FOCUS_LEFT;
116
117 case ArrowDown:
118 if(items.isEmpty() || selectedIndex == items.size() - 1) {
119 return Result.MOVE_FOCUS_DOWN;
120 }
121 selectedIndex++;
122 return Result.HANDLED;
123
124 case ArrowUp:
125 if(items.isEmpty() || selectedIndex == 0) {
126 return Result.MOVE_FOCUS_UP;
127 }
128 selectedIndex--;
129 return Result.HANDLED;
130
131 case Home:
132 selectedIndex = 0;
133 return Result.HANDLED;
134
135 case End:
136 selectedIndex = items.size() - 1;
137 return Result.HANDLED;
138
139 case PageUp:
140 if(getSize() != null) {
141 setSelectedIndex(getSelectedIndex() - getSize().getRows());
142 }
143 return Result.HANDLED;
144
145 case PageDown:
146 if(getSize() != null) {
147 setSelectedIndex(getSelectedIndex() + getSize().getRows());
148 }
149 return Result.HANDLED;
150
151 default:
152 }
153 return Result.UNHANDLED;
154 }
155 finally {
156 invalidate();
157 }
158 }
159
160 @Override
161 protected synchronized void afterEnterFocus(FocusChangeDirection direction, Interactable previouslyInFocus) {
162 if(items.isEmpty()) {
163 return;
164 }
165
166 if(direction == FocusChangeDirection.DOWN) {
167 selectedIndex = 0;
168 }
169 else if(direction == FocusChangeDirection.UP) {
170 selectedIndex = items.size() - 1;
171 }
172 }
173
174 /**
175 * Adds one more item to the list box, at the end.
176 * @param item Item to add to the list box
177 * @return Itself
178 */
179 public synchronized T addItem(V item) {
180 if(item == null) {
181 return self();
182 }
183
184 items.add(item);
185 if(selectedIndex == -1) {
186 selectedIndex = 0;
187 }
188 invalidate();
189 return self();
190 }
191
192 /**
193 * Removes all items from the list box
194 * @return Itself
195 */
196 public synchronized T clearItems() {
197 items.clear();
198 selectedIndex = -1;
199 invalidate();
200 return self();
201 }
202
203 /**
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
208 */
209 public synchronized int indexOf(V item) {
210 return items.indexOf(item);
211 }
212
213 /**
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
218 * the list box
219 */
220 public synchronized V getItemAt(int index) {
221 return items.get(index);
222 }
223
224 /**
225 * Checks if the list box has no items
226 * @return {@code true} if the list box has no items, {@code false} otherwise
227 */
228 public synchronized boolean isEmpty() {
229 return items.isEmpty();
230 }
231
232 /**
233 * Returns the number of items currently in the list box
234 * @return Number of items in the list box
235 */
236 public synchronized int getItemCount() {
237 return items.size();
238 }
239
240 /**
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
243 */
244 public synchronized List<V> getItems() {
245 return new ArrayList<V>(items);
246 }
247
248 /**
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
253 * @return Itself
254 */
255 public synchronized T setSelectedIndex(int index) {
256 selectedIndex = index;
257 if(selectedIndex < 0) {
258 selectedIndex = 0;
259 }
260 if(selectedIndex > items.size() - 1) {
261 selectedIndex = items.size() - 1;
262 }
263 invalidate();
264 return self();
265 }
266
267 /**
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
272 */
273 public int getSelectedIndex() {
274 return selectedIndex;
275 }
276
277 /**
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
282 */
283 public synchronized V getSelectedItem() {
284 if (selectedIndex == -1) {
285 return null;
286 } else {
287 return items.get(selectedIndex);
288 }
289 }
290
291 /**
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
295 */
296 public static class DefaultListBoxRenderer<V, T extends AbstractListBox<V, T>> implements InteractableRenderer<T> {
297 private int scrollTopIndex;
298
299 /**
300 * Default constructor
301 */
302 public DefaultListBoxRenderer() {
303 this.scrollTopIndex = 0;
304 }
305
306 @Override
307 public TerminalPosition getCursorLocation(T listBox) {
308 int selectedIndex = listBox.getSelectedIndex();
309 int columnAccordingToRenderer = listBox.getListItemRenderer().getHotSpotPositionOnLine(selectedIndex);
310 if(columnAccordingToRenderer == -1) {
311 return null;
312 }
313 return new TerminalPosition(columnAccordingToRenderer, selectedIndex - scrollTopIndex);
314 }
315
316 @Override
317 public TerminalSize getPreferredSize(T listBox) {
318 int maxWidth = 5; //Set it to something...
319 int index = 0;
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;
325 }
326 }
327 return new TerminalSize(maxWidth + 1, listBox.getItemCount());
328 }
329
330 @Override
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();
338
339 if(selectedIndex != -1) {
340 if(selectedIndex < scrollTopIndex)
341 scrollTopIndex = selectedIndex;
342 else if(selectedIndex >= componentHeight + scrollTopIndex)
343 scrollTopIndex = selectedIndex - componentHeight + 1;
344 }
345
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;
352 }
353
354 graphics.applyThemeStyle(graphics.getThemeDefinition(AbstractListBox.class).getNormal());
355 graphics.fill(' ');
356
357 TerminalSize itemSize = graphics.getSize().withRows(1);
358 for(int i = scrollTopIndex; i < items.size(); i++) {
359 if(i - scrollTopIndex >= componentHeight) {
360 break;
361 }
362 listItemRenderer.drawItem(
363 graphics.newTextGraphics(new TerminalPosition(0, i - scrollTopIndex), itemSize),
364 listBox,
365 i,
366 items.get(i),
367 selectedIndex == i,
368 listBox.isFocused());
369 }
370
371 graphics.applyThemeStyle(graphics.getThemeDefinition(AbstractListBox.class).getNormal());
372 if(items.size() > componentHeight) {
373 graphics.putString(componentWidth - 1, 0, Symbols.ARROW_UP + "");
374
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 + "");
378
379 graphics.applyThemeStyle(graphics.getThemeDefinition(AbstractListBox.class).getNormal());
380 graphics.putString(componentWidth - 1, componentHeight - 1, Symbols.ARROW_DOWN + "");
381
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, " ");
388 }
389 }
390 }
391
392 /**
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
397 */
398 public static class ListItemRenderer<V, T extends AbstractListBox<V, T>> {
399 /**
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
405 */
406 public int getHotSpotPositionOnLine(int selectedIndex) {
407 return 0;
408 }
409
410 /**
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
418 */
419 public String getLabel(T listBox, int index, V item) {
420 return item != null ? item.toString() : "<null>";
421 }
422
423 /**
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}
435 */
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());
439 }
440 else {
441 graphics.applyThemeStyle(graphics.getThemeDefinition(AbstractListBox.class).getNormal());
442 }
443 String label = getLabel(listBox, index, item);
444 label = TerminalTextUtils.fitString(label, graphics.getSize().getColumns());
445 graphics.putString(0, 0, label);
446 }
447 }
448}