Commit | Line | Data |
---|---|---|
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 | */ | |
19 | package com.googlecode.lanterna.gui2; | |
20 | ||
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; | |
26 | ||
27 | import java.util.ArrayList; | |
28 | import 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 | */ | |
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; | |
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 | } |