package com.github.swapii.condi.ui;
import com.github.swapii.condi.ui.tree.TreeItem;
import com.googlecode.lanterna.gui.TextGraphics;
import com.googlecode.lanterna.gui.component.AbstractInteractableComponent;
import com.googlecode.lanterna.input.Key;
import com.googlecode.lanterna.terminal.Terminal;
import com.googlecode.lanterna.terminal.TerminalPosition;
import com.googlecode.lanterna.terminal.TerminalSize;
import java.util.*;
/**
* @author Pavel Savinov
* @version 7/26/13 12:19 PM
*/
public class Tree extends AbstractInteractableComponent {
public static interface OnItemSelectedListener {
void itemSelected(TreeItem item);
}
private final Map<Integer, Boolean> hasChildsMap = new HashMap<Integer, Boolean>();
private final Map<Integer, Boolean> isSelectableMap = new HashMap<Integer, Boolean>();
private final Set<Integer> expandedSet = new HashSet<Integer>();
private final Map<Integer, List<TreeItem>> childsMap = new HashMap<Integer, List<TreeItem>>();
private TreeItem root;
private TreeItem selected;
private int height;
private int firstVisibleRow;
private int selectedRow;
private int itemsCount = Integer.MAX_VALUE;
private OnItemSelectedListener itemSelectedListener;
public Tree(TreeItem root) {
this.root = root;
}
@Override
public Result keyboardInteraction(Key key) {
switch (key.getKind()) {
case ArrowUp:
selectedRow--;
calculateVisibleRegion();
break;
case ArrowDown:
selectedRow++;
calculateVisibleRegion();
break;
case PageUp:
selectedRow -= height;
calculateVisibleRegion();
break;
case PageDown:
selectedRow += height;
calculateVisibleRegion();
break;
case ArrowRight:
if (selected != null) {
if (expanded(selected)) {
/*
* Select first child id already expanded
*/
selectedRow++;
selected = selected.getChilds().get(0);
calculateVisibleRegion();
} else {
if (hasChilds(selected) && isSelectable(selected)) {
expandedSet.add(selected.hashCode());
}
}
}
break;
case ArrowLeft:
if (selected != null) {
if (expanded(selected)) {
expandedSet.remove(selected.hashCode());
} else {
// Select parent if already collapsed or this is not leaf
TreeItem parent = selected.getParent();
if (parent.equals(root)) break;
int rows = 0;
for (TreeItem child : getChilds(parent)) {
rows++;
if (child.equals(selected)) {
break;
}
}
selected = parent;
selectedRow -= rows;
calculateVisibleRegion();
}
}
break;
case Tab:
return Result.NEXT_INTERACTABLE_DOWN;
case ReverseTab:
return Result.PREVIOUS_INTERACTABLE_UP;
default:
return Result.EVENT_NOT_HANDLED;
}
return Result.EVENT_HANDLED;
}
@Override
public void repaint(TextGraphics graphics) {
// Clear area
graphics.setBackgroundColor(Terminal.Color.DEFAULT);
graphics.setForegroundColor(Terminal.Color.WHITE);
graphics.fillArea(' ');
height = graphics.getHeight();
itemsCount = buildChilds(graphics, 0, root, -1);
if (itemsCount > 0) {
// Draw scroll bar
double proportion = (double) height / itemsCount;
int startRow = (int) (firstVisibleRow * proportion);
int fillRows = (int) (height * proportion) + 1;
graphics.setBackgroundColor(Terminal.Color.DEFAULT);
graphics.setForegroundColor(Terminal.Color.WHITE);
graphics.fillRectangle('░', new TerminalPosition(graphics.getWidth() - 1, 0), new TerminalSize(1, height));
graphics.fillRectangle('█', new TerminalPosition(graphics.getWidth() - 1, startRow), new TerminalSize(1, fillRows));
}
}
public void setOnItemSelectedListener(OnItemSelectedListener listener) {
itemSelectedListener = listener;
}
public TreeItem getSelectedItem() {
return selected;
}
@Override
protected TerminalSize calculatePreferredSize() {
return new TerminalSize(Integer.MAX_VALUE, Integer.MAX_VALUE);
}
private void calculateVisibleRegion() {
selectedRow = Math.max(0, selectedRow);
if (itemsCount > 0) selectedRow = Math.min(itemsCount - 1, selectedRow);
if (selectedRow - firstVisibleRow < height / 3) {
firstVisibleRow = Math.max(selectedRow - height / 3, 0);
}
if (selectedRow - firstVisibleRow > height * 2 / 3) {
firstVisibleRow = Math.min(selectedRow - height * 2 / 3, itemsCount - height);
}
}
/**
* Recursive method that goes throw items tree and render tree lines.
* In this method calculated overall items count.
*/
private int buildChilds(TextGraphics graphics, int row, TreeItem parent, int level) {
level++;
for (TreeItem item : getChilds(parent)) {
if (row == selectedRow) {
selected = item;
}
if (firstVisibleRow <= row && row < firstVisibleRow + graphics.getWidth()) {
if (isSelectable(item)) {
graphics.setForegroundColor(Terminal.Color.WHITE);
} else {
graphics.setForegroundColor(Terminal.Color.BLACK);
}
if (hasFocus() && row == selectedRow) {
graphics.setBackgroundColor(Terminal.Color.BLUE);
} else {
graphics.setBackgroundColor(Terminal.Color.DEFAULT);
}
String expandedPrefix = expanded(item) ? " -" : " +";
String prefix = hasChilds(item) ? expandedPrefix : " ";
StringBuilder b = new StringBuilder();
for (int i = 0; i < level; i++) b.append(" ");
b.append(prefix).append(' ').append(item.toString());
for (int i = b.length(); i < graphics.getWidth(); i++) b.append(' ');
graphics.drawString(0, row - firstVisibleRow, b.toString());
}
row++;
if (hasChilds(item) && expanded(item)) {
row = buildChilds(graphics, row, item, level);
}
}
return row;
}
private boolean isSelectable(TreeItem item) {
int hash = item.hashCode();
if (isSelectableMap.containsKey(hash)) {
return isSelectableMap.get(hash);
} else {
boolean selectable = item.isSelectable();
isSelectableMap.put(hash, selectable);
return selectable;
}
}
private boolean expanded(TreeItem item) {
return expandedSet.contains(item.hashCode());
}
private List<TreeItem> getChilds(TreeItem item) {
int hash = item.hashCode();
if (childsMap.containsKey(hash)) {
return childsMap.get(hash);
} else {
List<TreeItem> childs = item.getChilds();
childsMap.put(hash, childs);
return childs;
}
}
private boolean hasChilds(TreeItem item) {
int hash = item.hashCode();
if (hasChildsMap.containsKey(hash)) {
return hasChildsMap.get(hash);
} else {
boolean has = item.hasChilds();
hasChildsMap.put(hash, has);
return has;
}
}
}