Created
July 14, 2015 21:07
-
-
Save dant3/4a4249a886e4ec5625e6 to your computer and use it in GitHub Desktop.
Lanterna 2 ListView & ListAdapter - a very simple version
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
trait ListAdapter[+T] extends Reactive { | |
def isEmpty:Boolean = size <= 0 | |
def indexes:Range = 0 to maxIndex step 1 | |
def maxIndex:Int = size - 1 | |
def apply(index: Int):T = item(index) | |
def size:Int | |
def item(index: Int):T | |
def createItemString(index: Int): String | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import com.googlecode.lanterna.gui.Interactable.{FocusChangeDirection, Result} | |
import com.googlecode.lanterna.gui.Theme.Category | |
import com.googlecode.lanterna.gui.component.AbstractInteractableComponent | |
import com.googlecode.lanterna.gui.{Interactable, TextGraphics, Theme} | |
import com.googlecode.lanterna.input.Key | |
import com.googlecode.lanterna.input.Key.Kind._ | |
import com.googlecode.lanterna.terminal.{ACS, TerminalPosition, TerminalSize} | |
import scala.annotation.switch | |
class ListView[T](val adapter:ListAdapter[T]) extends AbstractInteractableComponent { | |
private var selection: Int = -1 | |
private var scrollTopIndex: Int = 0 | |
private var pageSize: Int = 0 | |
private var itemPressListener:(T) => Any = null | |
adapter.onChanged { () => invalidate() } | |
def onItemPressed(f:T => Any):ListView[T] = { | |
itemPressListener = f | |
this | |
} | |
override protected def calculatePreferredSize: TerminalSize = { | |
val size = adapter.size | |
val itemStringSizes = for {i <- adapter.indexes} yield adapter.createItemString(i).size | |
val maxItemSize = if (itemStringSizes.isEmpty) 0 else itemStringSizes.max | |
val maxWidth = math.max(maxItemSize, 5) | |
new TerminalSize(maxWidth + 1, size) | |
} | |
def selectedIndex_=(index: Int) { | |
selection = index | |
invalidate() | |
} | |
def selectedIndex = selection | |
override def isScrollable: Boolean = true | |
override def repaint(graphics: TextGraphics) = Renderer.repaint(graphics) | |
protected def getHotSpotPositionOnLine(selectedIndex: Int): Int = 0 | |
protected override def afterEnteredFocus(direction: Interactable.FocusChangeDirection) = if (!adapter.isEmpty) { | |
(direction: @switch) match { | |
case FocusChangeDirection.DOWN => selectedIndex = 0 | |
case FocusChangeDirection.UP => selectedIndex = adapter.size - 1 | |
case _ => | |
} | |
} | |
override def keyboardInteraction(key: Key): Interactable.Result = { | |
val keyHandlingResult = InputHandler.handleKey(key) | |
invalidate() | |
keyHandlingResult | |
} | |
object Renderer { | |
def repaint(graphics: TextGraphics) { | |
//update the page size, used for page up and page down keys | |
pageSize = graphics.getHeight | |
Renderer.updateScroll(graphics) | |
Renderer.renderItems(graphics) | |
Renderer.drawScrollBar(graphics) | |
Renderer.updateHotspot(graphics) | |
} | |
def updateScroll(graphics: TextGraphics) = { | |
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 | |
val itemsCount: Int = adapter.size | |
if (itemsCount > graphics.getHeight && itemsCount - scrollTopIndex < graphics.getHeight) { | |
scrollTopIndex = itemsCount - graphics.getHeight | |
} | |
graphics.applyTheme(getListItemThemeDefinition(graphics.getTheme)) | |
graphics.fillArea(' ') | |
} | |
def renderItems(graphics: TextGraphics) = { | |
val adapterIndexes = scrollTopIndex to math.min(adapter.maxIndex, graphics.getHeight + scrollTopIndex) | |
for (index <- adapterIndexes) { | |
if (index == selectedIndex && hasFocus) | |
graphics.applyTheme(getSelectedListItemThemeDefinition(graphics.getTheme)) | |
else | |
graphics.applyTheme(getListItemThemeDefinition(graphics.getTheme)) | |
Renderer.renderItem(graphics, 0, index - scrollTopIndex, index) | |
} | |
} | |
def drawScrollBar(graphics: TextGraphics) = { | |
val itemsCount = adapter.size | |
if (itemsCount > graphics.getHeight) { | |
graphics.applyTheme(Theme.Category.DIALOG_AREA) | |
graphics.drawString(graphics.getWidth - 1, 0, ACS.ARROW_UP + "") | |
graphics.applyTheme(Theme.Category.DIALOG_AREA) | |
for (row <- 1 to (graphics.getHeight - 1)) { | |
graphics.drawString(graphics.getWidth - 1, row, ACS.BLOCK_MIDDLE + "") | |
} | |
graphics.applyTheme(Theme.Category.DIALOG_AREA) | |
graphics.drawString(graphics.getWidth - 1, graphics.getHeight - 1, ACS.ARROW_DOWN + "") | |
val scrollableSize: Int = itemsCount - graphics.getHeight | |
val position: Double = scrollTopIndex.asInstanceOf[Double] / scrollableSize.asInstanceOf[Double] | |
val tickPosition: Int = ((graphics.getHeight.asInstanceOf[Double] - 3.0) * position).asInstanceOf[Int] | |
graphics.applyTheme(Theme.Category.SHADOW) | |
graphics.drawString(graphics.getWidth - 1, 1 + tickPosition, " ") | |
} | |
} | |
def updateHotspot(graphics: TextGraphics) = { | |
if (selectedIndex == -1 || adapter.isEmpty) { | |
setHotspot(new TerminalPosition(0, 0)) | |
} else { | |
val hotspotPosition = new TerminalPosition(getHotSpotPositionOnLine(selectedIndex), selectedIndex - scrollTopIndex) | |
setHotspot(graphics.translateToGlobalCoordinates(hotspotPosition)) | |
} | |
} | |
/** | |
* 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 def renderItem(graphics: TextGraphics, x: Int, y: Int, index: Int) { | |
def trimmedByGraphics(value: String) = if (value.length > graphics.getWidth) { | |
value.substring(0, graphics.getWidth) | |
} else { | |
value | |
} | |
graphics.drawString(x, y, trimmedByGraphics(adapter.createItemString(index))) | |
} | |
protected def getSelectedListItemThemeDefinition(theme: Theme): Theme.Definition = { | |
theme.getDefinition(Theme.Category.LIST_ITEM_SELECTED) | |
} | |
protected def getListItemThemeDefinition(theme: Theme): Theme.Definition = { | |
theme.getDefinition(Category.LIST_ITEM) | |
} | |
} | |
object InputHandler { | |
def handleKey(key: Key) = (key.getKind: @switch) match { | |
case Tab | ArrowRight => Result.NEXT_INTERACTABLE_RIGHT | |
case ReverseTab | ArrowLeft => Result.PREVIOUS_INTERACTABLE_LEFT | |
case ArrowDown => onArrowDown | |
case ArrowUp => onArrowUp | |
case Home => onHome | |
case End => onEnd | |
case PageUp => onPageUp | |
case PageDown => onPageDown | |
case Enter => onEnter | |
case _ => Result.EVENT_NOT_HANDLED | |
} | |
// Enter | |
def onEnter = if (adapter.indexes.contains(selectedIndex) && itemPressListener != null) { | |
itemPressListener(adapter.item(selectedIndex)) | |
Result.EVENT_HANDLED | |
} else { | |
Result.EVENT_NOT_HANDLED | |
} | |
// ▼ | |
def onArrowDown = if (adapter.isEmpty || selectedIndex == adapter.size - 1) { | |
Result.NEXT_INTERACTABLE_DOWN | |
} else { | |
selectedIndex += 1 | |
Result.EVENT_HANDLED | |
} | |
// ▲ | |
def onArrowUp = if (adapter.isEmpty || selectedIndex == 0) { | |
Result.PREVIOUS_INTERACTABLE_UP | |
} else { | |
selectedIndex -= 1 | |
if (selectedIndex - scrollTopIndex < 0) { | |
scrollTopIndex = selectedIndex | |
} | |
Result.EVENT_HANDLED | |
} | |
// HOME | |
def onHome = { | |
selectedIndex = 0 | |
Result.EVENT_HANDLED | |
} | |
// END | |
def onEnd = { | |
selectedIndex = adapter.maxIndex | |
Result.EVENT_HANDLED | |
} | |
// PAGE UP | |
def onPageUp = { | |
selectedIndex -= pageSize | |
if (selectedIndex < 0) { | |
selectedIndex = 0 | |
} | |
Result.EVENT_HANDLED | |
} | |
// PAGE DOWN | |
def onPageDown = { | |
selectedIndex += pageSize | |
if (selectedIndex > adapter.maxIndex) { | |
selectedIndex = adapter.maxIndex | |
} | |
Result.EVENT_HANDLED | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
trait Reactive { | |
private var changeListeners:Seq[() => Any] = Seq() | |
def onChanged(f:() => Any) = { | |
changeListeners = changeListeners :+ f | |
} | |
protected def notifyChanged() = for (changeListener <- changeListeners) { | |
changeListener() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment