| 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 | } |