/* * 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-2012 Martin */ package com.googlecode.lanterna.gui.component; import com.googlecode.lanterna.gui.Interactable; import com.googlecode.lanterna.gui.TextGraphics; import com.googlecode.lanterna.gui.Theme; import com.googlecode.lanterna.gui.Theme.Category; import com.googlecode.lanterna.input.Key; import com.googlecode.lanterna.terminal.ACS; import com.googlecode.lanterna.terminal.TerminalPosition; import com.googlecode.lanterna.terminal.TerminalSize; import java.util.ArrayList; import java.util.List; /** * Common base class for list-type components (check box list, action list, etc) * @author Martin */ public abstract class AbstractListBox extends AbstractInteractableComponent { private final List<Object> items; private TerminalSize preferredSizeOverride; private int selectedIndex; private int scrollTopIndex; public AbstractListBox() { this(null); } public AbstractListBox(TerminalSize preferredSize) { this.items = new ArrayList<Object>(); this.preferredSizeOverride = preferredSize; this.selectedIndex = -1; this.scrollTopIndex = 0; } @Override protected TerminalSize calculatePreferredSize() { if(preferredSizeOverride != null) return preferredSizeOverride; int maxWidth = 5; //Set it to something... for(int i = 0; i < items.size(); i++) { String itemString = createItemString(i); if(itemString.length() > maxWidth) maxWidth = itemString.length(); } return new TerminalSize(maxWidth + 1, items.size()); } protected void addItem(Object item) { if(item == null) return; items.add(item); if(selectedIndex == -1) selectedIndex = 0; invalidate(); } public void clearItems() { items.clear(); selectedIndex = -1; invalidate(); } public int indexOf(Object item) { return items.indexOf(item); } public int getSize() { return items.size(); } public Object getItemAt(int index) { return items.get(index); } public int getNrOfItems() { return items.size(); } public void setSelectedItem(int index) { selectedIndex = index; invalidate(); } public int getSelectedIndex() { return selectedIndex; } public Object getSelectedItem() { if(selectedIndex == -1) return null; else return items.get(selectedIndex); } @Override public boolean isScrollable() { return true; } public void repaint(TextGraphics graphics) { if(selectedIndex != -1) { if(selectedIndex < scrollTopIndex) scrollTopIndex = selectedIndex; else if(selectedIndex >= graphics.getHeight() + scrollTopIndex) scrollTopIndex = selectedIndex - graphics.getHeight() + 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() > graphics.getHeight() && items.size() - scrollTopIndex < graphics.getHeight()) { scrollTopIndex = items.size() - graphics.getHeight(); } graphics.applyTheme(getListItemThemeDefinition(graphics.getTheme())); graphics.fillArea(' '); for(int i = scrollTopIndex; i < items.size(); i++) { if(i - scrollTopIndex >= graphics.getHeight()) break; if(i == selectedIndex && hasFocus()) graphics.applyTheme(getSelectedListItemThemeDefinition(graphics.getTheme())); else graphics.applyTheme(getListItemThemeDefinition(graphics.getTheme())); printItem(graphics, 0, 0 + i - scrollTopIndex, i); } if(items.size() > graphics.getHeight()) { graphics.applyTheme(Theme.Category.DIALOG_AREA); graphics.drawString(graphics.getWidth() - 1, 0, ACS.ARROW_UP + ""); graphics.applyTheme(Theme.Category.DIALOG_AREA); for(int i = 1; i < graphics.getHeight() - 1; i++) graphics.drawString(graphics.getWidth() - 1, i, ACS.BLOCK_MIDDLE + ""); graphics.applyTheme(Theme.Category.DIALOG_AREA); graphics.drawString(graphics.getWidth() - 1, graphics.getHeight() - 1, ACS.ARROW_DOWN + ""); //Finally print the 'tick' int scrollableSize = items.size() - graphics.getHeight(); double position = (double)scrollTopIndex / ((double)scrollableSize); int tickPosition = (int)(((double)graphics.getHeight() - 3.0) * position); graphics.applyTheme(Theme.Category.SHADOW); graphics.drawString(graphics.getWidth() - 1, 1 + tickPosition, " "); } if(selectedIndex == -1 || items.isEmpty()) setHotspot(new TerminalPosition(0, 0)); else setHotspot(graphics.translateToGlobalCoordinates(new TerminalPosition(getHotSpotPositionOnLine(selectedIndex), selectedIndex - scrollTopIndex))); } @Override protected void afterEnteredFocus(FocusChangeDirection direction) { if(items.isEmpty()) return; if(direction == FocusChangeDirection.DOWN) selectedIndex = 0; else if(direction == FocusChangeDirection.UP) selectedIndex = items.size() - 1; } protected Theme.Definition getSelectedListItemThemeDefinition(Theme theme) { return theme.getDefinition(Theme.Category.LIST_ITEM_SELECTED); } protected Theme.Definition getListItemThemeDefinition(Theme theme) { return theme.getDefinition(Category.LIST_ITEM); } public Result keyboardInteraction(Key key) { try { switch(key.getKind()) { case Tab: case ArrowRight: return Result.NEXT_INTERACTABLE_RIGHT; case ReverseTab: case ArrowLeft: return Result.PREVIOUS_INTERACTABLE_LEFT; case ArrowDown: if(items.isEmpty() || selectedIndex == items.size() - 1) return Result.NEXT_INTERACTABLE_DOWN; selectedIndex++; break; case ArrowUp: if(items.isEmpty() || selectedIndex == 0) return Result.PREVIOUS_INTERACTABLE_UP; selectedIndex--; if(selectedIndex - scrollTopIndex < 0) scrollTopIndex--; break; default: return unhandledKeyboardEvent(key); } return Result.EVENT_HANDLED; } finally { invalidate(); } } /** * Draws an item in the ListBox at specific coordinates. If you override this method, * please note that the x and y positions have been pre-calculated for you and you should use * the values supplied instead of trying to figure out the position on your own based on the * index of the item. * @param graphics TextGraphics object to use when drawing the item * @param x X-coordinate on the terminal of the item, pre-adjusted for scrolling * @param y Y-coordinate on the terminal of the item, pre-adjusted for scrolling * @param index Index of the item that is to be drawn */ protected void printItem(TextGraphics graphics, int x, int y, int index) { String asText = createItemString(index); if(asText.length() > graphics.getWidth()) asText = asText.substring(0, graphics.getWidth()); graphics.drawString(x, y, asText); } protected Interactable.Result unhandledKeyboardEvent(Key key) { return Result.EVENT_NOT_HANDLED; } protected int getHotSpotPositionOnLine(int selectedIndex) { return 0; } protected abstract String createItemString(int index); }