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