/*
* 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.screen;
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.*;
/**
* A layer to put on top of a Terminal object, giving you a kind of screen buffer
* to use, which is a lot easier to work with. Drawing text or graphics to the
* terminal is kind of like writing to a bitmap.
* @author Martin
*/
public class Screen
{
private final Object mutex;
private final Terminal terminal;
private final LinkedList<TerminalSize> resizeQueue;
private TerminalPosition cursorPosition;
private TerminalSize terminalSize;
private ScreenCharacter [][] visibleScreen;
private ScreenCharacter [][] backbuffer;
private boolean wholeScreenInvalid;
private boolean hasBeenActivated;
//How to deal with \t characters
private TabBehaviour tabBehaviour;
/**
* Creates a new Screen on top of a supplied terminal, will query the terminal
* for its size. The screen is initially blank.
* @param terminal
* @throws LanternaException
*/
public Screen(Terminal terminal)
{
this(terminal, terminal.queryTerminalSize());
}
/**
* Creates a new Screen on top of a supplied terminal and will set the size
* of the screen to a supplied value. The screen is initially blank.
* @param terminal
* @param terminalSize
*/
public Screen(Terminal terminal, TerminalSize terminalSize)
{
this(terminal, terminalSize.getColumns(), terminalSize.getRows());
}
/**
* Creates a new Screen on top of a supplied terminal and will set the size
* of the screen to a supplied value. The screen is initially blank.
* @param terminal
* @param terminalWidth Width (number of columns) of the terminal
* @param terminalHeight Height (number of rows) of the terminal
*/
public Screen(Terminal terminal, int terminalWidth, int terminalHeight)
{
this.mutex = new Object();
this.terminal = terminal;
this.terminalSize = new TerminalSize(terminalWidth, terminalHeight);
this.visibleScreen = new ScreenCharacter[terminalHeight][terminalWidth];
this.backbuffer = new ScreenCharacter[terminalHeight][terminalWidth];
this.resizeQueue = new LinkedList<TerminalSize>();
this.wholeScreenInvalid = false;
this.hasBeenActivated = false;
this.cursorPosition = new TerminalPosition(0, 0);
this.tabBehaviour = TabBehaviour.ALIGN_TO_COLUMN_8;
this.terminal.addResizeListener(new TerminalResizeListener());
//Initialize the screen
clear();
}
/**
* @return The terminal which is the backend for this screen
*/
public Terminal getTerminal() {
return terminal;
}
/**
* @return Position where the cursor will be located after the screen has
* been refreshed or {@code null} if the cursor is not visible
*/
public TerminalPosition getCursorPosition()
{
return cursorPosition;
}
/**
* Moves the current cursor position or hides it. If the cursor is hidden and given a new
* position, it will be visible after this method call.
* @param position 0-indexed column and row numbers of the new position, or if {@code null},
* hides the cursor
*/
public void setCursorPosition(TerminalPosition position)
{
if(position != null)
//TerminalPosition isn't immutable, so make a copy
this.cursorPosition = new TerminalPosition(position);
else
this.cursorPosition = null;
}
/**
* Moves the current cursor position, and if the cursor was hidden it will be visible after this
* call
* @param column 0-indexed column number of the new position
* @param row 0-indexed row number of the new position
*/
public void setCursorPosition(int column, int row)
{
synchronized(mutex) {
if(column >= 0 && column < terminalSize.getColumns() &&
row >= 0 && row < terminalSize.getRows()) {
setCursorPosition(new TerminalPosition(column, row));
}
}
}
/**
* Sets the behaviour for what to do about tab characters.
* @see TabBehaviour
*/
public void setTabBehaviour(TabBehaviour tabBehaviour) {
if(tabBehaviour != null)
this.tabBehaviour = tabBehaviour;
}
/**
* Gets the behaviour for what to do about tab characters.
* @see TabBehaviour
*/
public TabBehaviour getTabBehaviour() {
return tabBehaviour;
}
/**
* Reads the next {@code Key} from the input queue, or returns null if there
* is nothing on the queue.
*/
public Key readInput()
{
return terminal.readInput();
}
/**
* @return Size of the screen
*/
public TerminalSize getTerminalSize()
{
synchronized(mutex) {
return terminalSize;
}
}
/**
* Calling this method will put the underlying terminal in private mode,
* clear the screen, move the cursor and refresh.
* @throws LanternaException
*/
public void startScreen()
{
if(hasBeenActivated)
return;
hasBeenActivated = true;
terminal.enterPrivateMode();
terminal.clearScreen();
if(cursorPosition != null) {
terminal.setCursorVisible(true);
terminal.moveCursor(cursorPosition.getColumn(), cursorPosition.getRow());
}
else
terminal.setCursorVisible(false);
refresh();
}
/**
* Calling this method will make the underlying terminal leave private mode,
* effectively going back to whatever state the terminal was in before
* calling {@code startScreen()}
* @throws LanternaException
*/
public void stopScreen()
{
if(!hasBeenActivated)
return;
terminal.exitPrivateMode();
hasBeenActivated = false;
}
/**
* Erases all the characters on the screen, effectively giving you a blank
* area. The default background color will be used, if you want to fill the
* screen with a different color you will need to do this manually.
*/
public void clear() {
//ScreenCharacter is immutable, so we can use it for every element
ScreenCharacter background = new ScreenCharacter(' ');
synchronized(mutex) {
for(int y = 0; y < terminalSize.getRows(); y++) {
for(int x = 0; x < terminalSize.getColumns(); x++) {
backbuffer[y][x] = background;
}
}
}
}
/**
* Draws a string on the screen at a particular position
* @param x 0-indexed column number of where to put the first character in the string
* @param y 0-indexed row number of where to put the first character in the string
* @param string Text to put on the screen
* @param foregroundColor What color to use for the text
* @param backgroundColor What color to use for the background
* @param styles Additional styles to apply to the text
*/
public void putString(int x, int y, String string, Terminal.Color foregroundColor,
Terminal.Color backgroundColor, ScreenCharacterStyle... styles)
{
Set<ScreenCharacterStyle> drawStyle = EnumSet.noneOf(ScreenCharacterStyle.class);
drawStyle.addAll(Arrays.asList(styles));
putString(x, y, string, foregroundColor, backgroundColor, drawStyle);
}
/**
* Draws a string on the screen at a particular position
* @param x 0-indexed column number of where to put the first character in the string
* @param y 0-indexed row number of where to put the first character in the string
* @param string Text to put on the screen
* @param foregroundColor What color to use for the text
* @param backgroundColor What color to use for the background
* @param styles Additional styles to apply to the text
*/
public void putString(int x, int y, String string, Terminal.Color foregroundColor,
Terminal.Color backgroundColor, Set<ScreenCharacterStyle> styles)
{
string = tabBehaviour.replaceTabs(string, x);
for(int i = 0; i < string.length(); i++)
putCharacter(x + i, y,
new ScreenCharacter(string.charAt(i),
foregroundColor,
backgroundColor,
styles));
}
void putCharacter(int x, int y, ScreenCharacter character)
{
synchronized(mutex) {
if(y < 0 || y >= backbuffer.length || x < 0 || x >= backbuffer[0].length)
return;
//Only create a new character if the
if(!backbuffer[y][x].equals(character))
backbuffer[y][x] = new ScreenCharacter(character);
}
}
/**
* This method will check if there are any resize commands pending. If true,
* you need to call refresh() to perform the screen resize
* @return true if the size is the same as before, false if the screen should be resized
*/
public boolean resizePending()
{
synchronized(resizeQueue) {
return !resizeQueue.isEmpty();
}
}
/**
* Call this method to make changes done through {@code putCharacter(...)},
* {@code putString(...)} visible on the terminal. The screen will calculate
* the changes that are required and send the necessary characters and
* control sequences to make it so.
*/
public void refresh()
{
if(!hasBeenActivated)
return;
synchronized(mutex) {
//If any resize operations are in the queue, execute them
resizeScreenIfNeeded();
Map<TerminalPosition, ScreenCharacter> updateMap = new TreeMap<TerminalPosition, ScreenCharacter>(new ScreenPointComparator());
for(int y = 0; y < terminalSize.getRows(); y++)
{
for(int x = 0; x < terminalSize.getColumns(); x++)
{
ScreenCharacter c = backbuffer[y][x];
if(!c.equals(visibleScreen[y][x]) || wholeScreenInvalid) {
visibleScreen[y][x] = c; //Remember, ScreenCharacter is immutable, we don't need to worry about it being modified
updateMap.put(new TerminalPosition(x, y), c);
}
}
}
Writer terminalWriter = new Writer();
terminalWriter.reset();
TerminalPosition previousPoint = null;
for(TerminalPosition nextUpdate: updateMap.keySet()) {
if(previousPoint == null || previousPoint.getRow() != nextUpdate.getRow() ||
previousPoint.getColumn() + 1 != nextUpdate.getColumn()) {
terminalWriter.setCursorPosition(nextUpdate.getColumn(), nextUpdate.getRow());
}
terminalWriter.writeCharacter(updateMap.get(nextUpdate));
previousPoint = nextUpdate;
}
if(cursorPosition != null) {
terminalWriter.setCursorVisible(true);
terminalWriter.setCursorPosition(cursorPosition.getColumn(), cursorPosition.getRow());
}
else {
terminalWriter.setCursorVisible(false);
}
wholeScreenInvalid = false;
}
terminal.flush();
}
//WARNING!!! Should only be called in a block synchronized on mutex! See refresh()
private void resizeScreenIfNeeded() {
TerminalSize newSize;
synchronized(resizeQueue) {
if(resizeQueue.isEmpty())
return;
newSize = resizeQueue.getLast();
resizeQueue.clear();
}
int height = newSize.getRows();
int width = newSize.getColumns();
ScreenCharacter [][]newBackBuffer = new ScreenCharacter[height][width];
ScreenCharacter [][]newVisibleScreen = new ScreenCharacter[height][width];
ScreenCharacter newAreaCharacter = new ScreenCharacter('X', Terminal.Color.GREEN, Terminal.Color.BLACK);
for(int y = 0; y < height; y++)
{
for(int x = 0; x < width; x++)
{
if(backbuffer.length > 0 && x < backbuffer[0].length && y < backbuffer.length)
newBackBuffer[y][x] = backbuffer[y][x];
else
newBackBuffer[y][x] = new ScreenCharacter(newAreaCharacter);
if(visibleScreen.length > 0 && x < visibleScreen[0].length && y < visibleScreen.length)
newVisibleScreen[y][x] = visibleScreen[y][x];
else
newVisibleScreen[y][x] = new ScreenCharacter(newAreaCharacter);
}
}
backbuffer = newBackBuffer;
visibleScreen = newVisibleScreen;
wholeScreenInvalid = true;
terminalSize = new TerminalSize(newSize);
}
private static class ScreenPointComparator implements Comparator<TerminalPosition>
{
public int compare(TerminalPosition o1, TerminalPosition o2)
{
if(o1.getRow() == o2.getRow())
if(o1.getColumn() == o2.getColumn())
return 0;
else
return new Integer(o1.getColumn()).compareTo(o2.getColumn());
else
return new Integer(o1.getRow()).compareTo(o2.getRow());
}
}
private class TerminalResizeListener implements Terminal.ResizeListener
{
public void onResized(TerminalSize newSize)
{
synchronized(resizeQueue) {
resizeQueue.add(newSize);
}
}
}
private class Writer
{
private Terminal.Color currentForegroundColor;
private Terminal.Color currentBackgroundColor;
private boolean currentlyIsBold;
private boolean currentlyIsUnderline;
private boolean currentlyIsNegative;
private boolean currentlyIsBlinking;
public Writer()
{
currentForegroundColor = Terminal.Color.DEFAULT;
currentBackgroundColor = Terminal.Color.DEFAULT;
currentlyIsBold = false;
currentlyIsUnderline = false;
currentlyIsNegative = false;
currentlyIsBlinking = false;
}
void setCursorPosition(int x, int y)
{
terminal.moveCursor(x, y);
}
private void setCursorVisible(boolean visible) {
terminal.setCursorVisible(visible);
}
void writeCharacter(ScreenCharacter character)
{
if (currentlyIsBlinking != character.isBlinking()) {
if (character.isBlinking()) {
terminal.applySGR(Terminal.SGR.ENTER_BLINK);
currentlyIsBlinking = true;
}
else {
terminal.applySGR(Terminal.SGR.RESET_ALL);
terminal.applyBackgroundColor(character.getBackgroundColor());
terminal.applyForegroundColor(character.getForegroundColor());
// emulating "stop_blink_mode" so that previous formatting is preserved
currentlyIsBold = false;
currentlyIsUnderline = false;
currentlyIsNegative = false;
currentlyIsBlinking = false;
}
}
if(currentForegroundColor != character.getForegroundColor()) {
terminal.applyForegroundColor(character.getForegroundColor());
currentForegroundColor = character.getForegroundColor();
}
if(currentBackgroundColor != character.getBackgroundColor()) {
terminal.applyBackgroundColor(character.getBackgroundColor());
currentBackgroundColor = character.getBackgroundColor();
}
if(currentlyIsBold != character.isBold()) {
if(character.isBold()) {
terminal.applySGR(Terminal.SGR.ENTER_BOLD);
currentlyIsBold = true;
}
else {
terminal.applySGR(Terminal.SGR.EXIT_BOLD);
currentlyIsBold = false;
}
}
if(currentlyIsUnderline != character.isUnderline()) {
if(character.isUnderline()) {
terminal.applySGR(Terminal.SGR.ENTER_UNDERLINE);
currentlyIsUnderline = true;
}
else {
terminal.applySGR(Terminal.SGR.EXIT_UNDERLINE);
currentlyIsUnderline = false;
}
}
if(currentlyIsNegative != character.isNegative()) {
if(character.isNegative()) {
terminal.applySGR(Terminal.SGR.ENTER_REVERSE);
currentlyIsNegative = true;
}
else {
terminal.applySGR(Terminal.SGR.EXIT_REVERSE);
currentlyIsNegative = false;
}
}
terminal.putCharacter(character.getCharacter());
}
void reset()
{
terminal.applySGR(Terminal.SGR.RESET_ALL);
terminal.moveCursor(0, 0);
currentBackgroundColor = Terminal.Color.DEFAULT;
currentForegroundColor = Terminal.Color.DEFAULT;
currentlyIsBold = false;
currentlyIsNegative = false;
currentlyIsUnderline = false;
}
}
}