+++ /dev/null
-/*
- * 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 <http://www.gnu.org/licenses/>.
- *
- * 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 <T> Should always be itself, see {@code AbstractComponent}
- * @param <V> Type of items this list box contains
- * @author Martin
- */
-public abstract class AbstractListBox<V, T extends AbstractListBox<V, T>> extends AbstractInteractableComponent<T> {
- private final List<V> items;
- private int selectedIndex;
- private ListItemRenderer<V,T> 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<V>();
- this.selectedIndex = -1;
- setPreferredSize(size);
- setListItemRenderer(createDefaultListItemRenderer());
- }
-
- @Override
- protected InteractableRenderer<T> createDefaultRenderer() {
- return new DefaultListBoxRenderer<V, T>();
- }
-
- /**
- * 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<V,T> createDefaultListItemRenderer() {
- return new ListItemRenderer<V,T>();
- }
-
- ListItemRenderer<V,T> 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<V,T> 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<V> getItems() {
- return new ArrayList<V>(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 <V> Type of the items the list box this renderer is for
- * @param <T> Type of list box
- */
- public static class DefaultListBoxRenderer<V, T extends AbstractListBox<V, T>> implements InteractableRenderer<T> {
- 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<V> items = listBox.getItems();
- ListItemRenderer<V,T> 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 <V> Type of the items in the list box
- * @param <T> Type of the list box class itself
- */
- public static class ListItemRenderer<V, T extends AbstractListBox<V, T>> {
- /**
- * 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() : "<null>";
- }
-
- /**
- * 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);
- }
- }
-}