/* * This file is part of lanterna (http://code.google.com/p/lanterna/). * * lanterna is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . * * Copyright (C) 2010-2015 Martin */ package com.googlecode.lanterna.gui2; import com.googlecode.lanterna.TerminalTextUtils; import com.googlecode.lanterna.Symbols; import com.googlecode.lanterna.TerminalPosition; import com.googlecode.lanterna.TerminalSize; import com.googlecode.lanterna.input.KeyStroke; import java.util.ArrayList; import java.util.List; /** * Base class for several list box implementations, this will handle things like list of items and the scrollbar. * @param Should always be itself, see {@code AbstractComponent} * @param Type of items this list box contains * @author Martin */ public abstract class AbstractListBox> extends AbstractInteractableComponent { private final List items; private int selectedIndex; private ListItemRenderer listItemRenderer; /** * This constructor sets up the component so it has no preferred size but will ask to be as big as the list is. If * the GUI cannot accommodate this size, scrolling and a vertical scrollbar will be used. */ protected AbstractListBox() { this(null); } /** * This constructor sets up the component with a preferred size that is will always request, no matter what items * are in the list box. If there are more items than the size can contain, scrolling and a vertical scrollbar will * be used. Calling this constructor with a {@code null} value has the same effect as calling the default * constructor. * * @param size Preferred size that the list should be asking for instead of invoking the preferred size calculation, * or if set to {@code null} will ask to be big enough to display all items. */ protected AbstractListBox(TerminalSize size) { this.items = new ArrayList(); this.selectedIndex = -1; setPreferredSize(size); setListItemRenderer(createDefaultListItemRenderer()); } @Override protected InteractableRenderer createDefaultRenderer() { return new DefaultListBoxRenderer(); } /** * Method that constructs the {@code ListItemRenderer} that this list box should use to draw the elements of the * list box. This can be overridden to supply a custom renderer. Note that this is not the renderer used for the * entire list box but for each item, called one by one. * @return {@code ListItemRenderer} to use when drawing the items in the list */ protected ListItemRenderer createDefaultListItemRenderer() { return new ListItemRenderer(); } ListItemRenderer getListItemRenderer() { return listItemRenderer; } /** * This method overrides the {@code ListItemRenderer} that is used to draw each element in the list box. Note that * this is not the renderer used for the entire list box but for each item, called one by one. * @param listItemRenderer New renderer to use when drawing the items in the list box * @return Itself */ public synchronized T setListItemRenderer(ListItemRenderer listItemRenderer) { if(listItemRenderer == null) { listItemRenderer = createDefaultListItemRenderer(); if(listItemRenderer == null) { throw new IllegalStateException("createDefaultListItemRenderer returned null"); } } this.listItemRenderer = listItemRenderer; return self(); } @Override public synchronized Result handleKeyStroke(KeyStroke keyStroke) { try { switch(keyStroke.getKeyType()) { case Tab: return Result.MOVE_FOCUS_NEXT; case ReverseTab: return Result.MOVE_FOCUS_PREVIOUS; case ArrowRight: return Result.MOVE_FOCUS_RIGHT; case ArrowLeft: return Result.MOVE_FOCUS_LEFT; case ArrowDown: if(items.isEmpty() || selectedIndex == items.size() - 1) { return Result.MOVE_FOCUS_DOWN; } selectedIndex++; return Result.HANDLED; case ArrowUp: if(items.isEmpty() || selectedIndex == 0) { return Result.MOVE_FOCUS_UP; } selectedIndex--; return Result.HANDLED; case Home: selectedIndex = 0; return Result.HANDLED; case End: selectedIndex = items.size() - 1; return Result.HANDLED; case PageUp: if(getSize() != null) { setSelectedIndex(getSelectedIndex() - getSize().getRows()); } return Result.HANDLED; case PageDown: if(getSize() != null) { setSelectedIndex(getSelectedIndex() + getSize().getRows()); } return Result.HANDLED; default: } return Result.UNHANDLED; } finally { invalidate(); } } @Override protected synchronized void afterEnterFocus(FocusChangeDirection direction, Interactable previouslyInFocus) { if(items.isEmpty()) { return; } if(direction == FocusChangeDirection.DOWN) { selectedIndex = 0; } else if(direction == FocusChangeDirection.UP) { selectedIndex = items.size() - 1; } } /** * Adds one more item to the list box, at the end. * @param item Item to add to the list box * @return Itself */ public synchronized T addItem(V item) { if(item == null) { return self(); } items.add(item); if(selectedIndex == -1) { selectedIndex = 0; } invalidate(); return self(); } /** * Removes all items from the list box * @return Itself */ public synchronized T clearItems() { items.clear(); selectedIndex = -1; invalidate(); return self(); } /** * Looks for the particular item in the list and returns the index within the list (starting from zero) of that item * if it is found, or -1 otherwise * @param item What item to search for in the list box * @return Index of the item in the list box or -1 if the list box does not contain the item */ public synchronized int indexOf(V item) { return items.indexOf(item); } /** * Retrieves the item at the specified index in the list box * @param index Index of the item to fetch * @return The item at the specified index * @throws IndexOutOfBoundsException If the index is less than zero or equals/greater than the number of items in * the list box */ public synchronized V getItemAt(int index) { return items.get(index); } /** * Checks if the list box has no items * @return {@code true} if the list box has no items, {@code false} otherwise */ public synchronized boolean isEmpty() { return items.isEmpty(); } /** * Returns the number of items currently in the list box * @return Number of items in the list box */ public synchronized int getItemCount() { return items.size(); } /** * Returns a copy of the items in the list box as a {@code List} * @return Copy of all the items in this list box */ public synchronized List getItems() { return new ArrayList(items); } /** * Sets which item in the list box that is currently selected. Please note that in this context, selected simply * means it is the item that currently has input focus. This is not to be confused with list box implementations * such as {@code CheckBoxList} where individual items have a certain checked/unchecked state. * @param index Index of the item that should be currently selected * @return Itself */ public synchronized T setSelectedIndex(int index) { selectedIndex = index; if(selectedIndex < 0) { selectedIndex = 0; } if(selectedIndex > items.size() - 1) { selectedIndex = items.size() - 1; } invalidate(); return self(); } /** * Returns the index of the currently selected item in the list box. Please note that in this context, selected * simply means it is the item that currently has input focus. This is not to be confused with list box * implementations such as {@code CheckBoxList} where individual items have a certain checked/unchecked state. * @return The index of the currently selected row in the list box, or -1 if there are no items */ public int getSelectedIndex() { return selectedIndex; } /** * Returns the currently selected item in the list box. Please note that in this context, selected * simply means it is the item that currently has input focus. This is not to be confused with list box * implementations such as {@code CheckBoxList} where individual items have a certain checked/unchecked state. * @return The currently selected item in the list box, or {@code null} if there are no items */ public synchronized V getSelectedItem() { if (selectedIndex == -1) { return null; } else { return items.get(selectedIndex); } } /** * The default renderer for {@code AbstractListBox} and all its subclasses. * @param Type of the items the list box this renderer is for * @param Type of list box */ public static class DefaultListBoxRenderer> implements InteractableRenderer { private int scrollTopIndex; /** * Default constructor */ public DefaultListBoxRenderer() { this.scrollTopIndex = 0; } @Override public TerminalPosition getCursorLocation(T listBox) { int selectedIndex = listBox.getSelectedIndex(); int columnAccordingToRenderer = listBox.getListItemRenderer().getHotSpotPositionOnLine(selectedIndex); if(columnAccordingToRenderer == -1) { return null; } return new TerminalPosition(columnAccordingToRenderer, selectedIndex - scrollTopIndex); } @Override public TerminalSize getPreferredSize(T listBox) { int maxWidth = 5; //Set it to something... int index = 0; for (V item : listBox.getItems()) { String itemString = listBox.getListItemRenderer().getLabel(listBox, index++, item); int stringLengthInColumns = TerminalTextUtils.getColumnWidth(itemString); if (stringLengthInColumns > maxWidth) { maxWidth = stringLengthInColumns; } } return new TerminalSize(maxWidth + 1, listBox.getItemCount()); } @Override public void drawComponent(TextGUIGraphics graphics, T listBox) { //update the page size, used for page up and page down keys int componentHeight = graphics.getSize().getRows(); int componentWidth = graphics.getSize().getColumns(); int selectedIndex = listBox.getSelectedIndex(); List items = listBox.getItems(); ListItemRenderer listItemRenderer = listBox.getListItemRenderer(); if(selectedIndex != -1) { if(selectedIndex < scrollTopIndex) scrollTopIndex = selectedIndex; else if(selectedIndex >= componentHeight + scrollTopIndex) scrollTopIndex = selectedIndex - componentHeight + 1; } //Do we need to recalculate the scroll position? //This code would be triggered by resizing the window when the scroll //position is at the bottom if(items.size() > componentHeight && items.size() - scrollTopIndex < componentHeight) { scrollTopIndex = items.size() - componentHeight; } graphics.applyThemeStyle(graphics.getThemeDefinition(AbstractListBox.class).getNormal()); graphics.fill(' '); TerminalSize itemSize = graphics.getSize().withRows(1); for(int i = scrollTopIndex; i < items.size(); i++) { if(i - scrollTopIndex >= componentHeight) { break; } listItemRenderer.drawItem( graphics.newTextGraphics(new TerminalPosition(0, i - scrollTopIndex), itemSize), listBox, i, items.get(i), selectedIndex == i, listBox.isFocused()); } graphics.applyThemeStyle(graphics.getThemeDefinition(AbstractListBox.class).getNormal()); if(items.size() > componentHeight) { graphics.putString(componentWidth - 1, 0, Symbols.ARROW_UP + ""); graphics.applyThemeStyle(graphics.getThemeDefinition(AbstractListBox.class).getInsensitive()); for(int i = 1; i < componentHeight - 1; i++) graphics.putString(componentWidth - 1, i, Symbols.BLOCK_MIDDLE + ""); graphics.applyThemeStyle(graphics.getThemeDefinition(AbstractListBox.class).getNormal()); graphics.putString(componentWidth - 1, componentHeight - 1, Symbols.ARROW_DOWN + ""); //Finally print the 'tick' int scrollableSize = items.size() - componentHeight; double position = (double)scrollTopIndex / ((double)scrollableSize); int tickPosition = (int)(((double) componentHeight - 3.0) * position); graphics.applyThemeStyle(graphics.getThemeDefinition(AbstractListBox.class).getInsensitive()); graphics.putString(componentWidth - 1, 1 + tickPosition, " "); } } } /** * The default list item renderer class, this can be extended and customized it needed. The instance which is * assigned to the list box will be called once per item in the list when the list box is drawn. * @param Type of the items in the list box * @param Type of the list box class itself */ public static class ListItemRenderer> { /** * Returns where on the line to place the text terminal cursor for a currently selected item. By default this * will return 0, meaning the first character of the selected line. If you extend {@code ListItemRenderer} you * can change this by returning a different number. Returning -1 will cause lanterna to hide the cursor. * @param selectedIndex Which item is currently selected * @return Index of the character in the string we want to place the terminal cursor on, or -1 to hide it */ public int getHotSpotPositionOnLine(int selectedIndex) { return 0; } /** * Given a list box, an index of an item within that list box and what the item is, this method should return * what to draw for that item. The default implementation is to return whatever {@code toString()} returns when * called on the item. * @param listBox List box the item belongs to * @param index Index of the item * @param item The item itself * @return String to draw for this item */ public String getLabel(T listBox, int index, V item) { return item != null ? item.toString() : ""; } /** * This is the main drawing method for a single list box item, it applies the current theme to setup the colors * and then calls {@code getLabel(..)} and draws the result using the supplied {@code TextGUIGraphics}. The * graphics object is created just for this item and is restricted so that it can only draw on the area this * item is occupying. The top-left corner (0x0) should be the starting point when drawing the item. * @param graphics Graphics object to draw with * @param listBox List box we are drawing an item from * @param index Index of the item we are drawing * @param item The item we are drawing * @param selected Will be set to {@code true} if the item is currently selected, otherwise {@code false}, but * please notice what context 'selected' refers to here (see {@code setSelectedIndex}) * @param focused Will be set to {@code true} if the list box currently has input focus, otherwise {@code false} */ public void drawItem(TextGUIGraphics graphics, T listBox, int index, V item, boolean selected, boolean focused) { if(selected && focused) { graphics.applyThemeStyle(graphics.getThemeDefinition(AbstractListBox.class).getSelected()); } else { graphics.applyThemeStyle(graphics.getThemeDefinition(AbstractListBox.class).getNormal()); } String label = getLabel(listBox, index, item); label = TerminalTextUtils.fitString(label, graphics.getSize().getColumns()); graphics.putString(0, 0, label); } } }