/*
* 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-2017 Martin Berglund
*/
package com.googlecode.lanterna.screen;
import com.googlecode.lanterna.TerminalTextUtils;
import com.googlecode.lanterna.TerminalSize;
import com.googlecode.lanterna.TextCharacter;
import com.googlecode.lanterna.graphics.TextGraphics;
import com.googlecode.lanterna.TerminalPosition;
import com.googlecode.lanterna.graphics.TextImage;
import java.io.IOException;
/**
* This class implements some of the Screen logic that is not directly tied to the actual implementation of how the
* Screen translate to the terminal. It keeps data structures for the front- and back buffers, the cursor location and
* some other simpler states.
* @author martin
*/
public abstract class AbstractScreen implements Screen {
private TerminalPosition cursorPosition;
private ScreenBuffer backBuffer;
private ScreenBuffer frontBuffer;
private final TextCharacter defaultCharacter;
//How to deal with \t characters
private TabBehaviour tabBehaviour;
//Current size of the screen
private TerminalSize terminalSize;
//Pending resize of the screen
private TerminalSize latestResizeRequest;
public AbstractScreen(TerminalSize initialSize) {
this(initialSize, DEFAULT_CHARACTER);
}
/**
* Creates a new Screen on top of a supplied terminal, will query the terminal for its size. The screen is initially
* blank. You can specify which character you wish to be used to fill the screen initially; this will also be the
* character used if the terminal is enlarged and you don't set anything on the new areas.
*
* @param initialSize Size to initially create the Screen with (can be resized later)
* @param defaultCharacter What character to use for the initial state of the screen and expanded areas
*/
@SuppressWarnings({"SameParameterValue", "WeakerAccess"})
public AbstractScreen(TerminalSize initialSize, TextCharacter defaultCharacter) {
this.frontBuffer = new ScreenBuffer(initialSize, defaultCharacter);
this.backBuffer = new ScreenBuffer(initialSize, defaultCharacter);
this.defaultCharacter = defaultCharacter;
this.cursorPosition = new TerminalPosition(0, 0);
this.tabBehaviour = TabBehaviour.ALIGN_TO_COLUMN_4;
this.terminalSize = initialSize;
this.latestResizeRequest = null;
}
/**
* @return Position where the cursor will be located after the screen has been refreshed or {@code null} if the
* cursor is not visible
*/
@Override
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
*/
@Override
public void setCursorPosition(TerminalPosition position) {
if(position == null) {
//Skip any validation checks if we just want to hide the cursor
this.cursorPosition = null;
return;
}
if(position.getColumn() < 0) {
position = position.withColumn(0);
}
if(position.getRow() < 0) {
position = position.withRow(0);
}
if(position.getColumn() >= terminalSize.getColumns()) {
position = position.withColumn(terminalSize.getColumns() - 1);
}
if(position.getRow() >= terminalSize.getRows()) {
position = position.withRow(terminalSize.getRows() - 1);
}
this.cursorPosition = position;
}
@Override
public void setTabBehaviour(TabBehaviour tabBehaviour) {
if(tabBehaviour != null) {
this.tabBehaviour = tabBehaviour;
}
}
@Override
public TabBehaviour getTabBehaviour() {
return tabBehaviour;
}
@Override
public void setCharacter(TerminalPosition position, TextCharacter screenCharacter) {
setCharacter(position.getColumn(), position.getRow(), screenCharacter);
}
@Override
public TextGraphics newTextGraphics() {
return new ScreenTextGraphics(this) {
@Override
public TextGraphics drawImage(TerminalPosition topLeft, TextImage image, TerminalPosition sourceImageTopLeft, TerminalSize sourceImageSize) {
backBuffer.copyFrom(image, sourceImageTopLeft.getRow(), sourceImageSize.getRows(), sourceImageTopLeft.getColumn(), sourceImageSize.getColumns(), topLeft.getRow(), topLeft.getColumn());
return this;
}
};
}
@Override
public synchronized void setCharacter(int column, int row, TextCharacter screenCharacter) {
//It would be nice if we didn't have to care about tabs at this level, but we have no such luxury
if(screenCharacter.getCharacter() == '\t') {
//Swap out the tab for a space
screenCharacter = screenCharacter.withCharacter(' ');
//Now see how many times we have to put spaces...
for(int i = 0; i < tabBehaviour.replaceTabs("\t", column).length(); i++) {
backBuffer.setCharacterAt(column + i, row, screenCharacter);
}
}
else {
//This is the normal case, no special character
backBuffer.setCharacterAt(column, row, screenCharacter);
}
}
@Override
public synchronized TextCharacter getFrontCharacter(TerminalPosition position) {
return getFrontCharacter(position.getColumn(), position.getRow());
}
@Override
public TextCharacter getFrontCharacter(int column, int row) {
return getCharacterFromBuffer(frontBuffer, column, row);
}
@Override
public synchronized TextCharacter getBackCharacter(TerminalPosition position) {
return getBackCharacter(position.getColumn(), position.getRow());
}
@Override
public TextCharacter getBackCharacter(int column, int row) {
return getCharacterFromBuffer(backBuffer, column, row);
}
@Override
public void refresh() throws IOException {
refresh(RefreshType.AUTOMATIC);
}
@Override
public void close() throws IOException {
stopScreen();
}
@Override
public synchronized void clear() {
backBuffer.setAll(defaultCharacter);
}
@Override
public synchronized TerminalSize doResizeIfNecessary() {
TerminalSize pendingResize = getAndClearPendingResize();
if(pendingResize == null) {
return null;
}
backBuffer = backBuffer.resize(pendingResize, defaultCharacter);
frontBuffer = frontBuffer.resize(pendingResize, defaultCharacter);
return pendingResize;
}
@Override
public TerminalSize getTerminalSize() {
return terminalSize;
}
/**
* Returns the front buffer connected to this screen, don't use this unless you know what you are doing!
* @return This Screen's front buffer
*/
protected ScreenBuffer getFrontBuffer() {
return frontBuffer;
}
/**
* Returns the back buffer connected to this screen, don't use this unless you know what you are doing!
* @return This Screen's back buffer
*/
protected ScreenBuffer getBackBuffer() {
return backBuffer;
}
private synchronized TerminalSize getAndClearPendingResize() {
if(latestResizeRequest != null) {
terminalSize = latestResizeRequest;
latestResizeRequest = null;
return terminalSize;
}
return null;
}
/**
* Tells this screen that the size has changed and it should, at next opportunity, resize itself and its buffers
* @param newSize New size the 'real' terminal now has
*/
protected void addResizeRequest(TerminalSize newSize) {
latestResizeRequest = newSize;
}
private TextCharacter getCharacterFromBuffer(ScreenBuffer buffer, int column, int row) {
if(column > 0) {
//If we are picking the padding of a CJK character, pick the actual CJK character instead of the padding
TextCharacter leftOfSpecifiedCharacter = buffer.getCharacterAt(column - 1, row);
if(leftOfSpecifiedCharacter == null) {
//If the character left of us doesn't exist, we don't exist either
return null;
}
else if(TerminalTextUtils.isCharCJK(leftOfSpecifiedCharacter.getCharacter())) {
return leftOfSpecifiedCharacter;
}
}
return buffer.getCharacterAt(column, row);
}
@Override
public String toString() {
return getBackBuffer().toString();
}
/**
* Performs the scrolling on its back-buffer.
*/
@Override
public void scrollLines(int firstLine, int lastLine, int distance) {
getBackBuffer().scrollLines(firstLine, lastLine, distance);
}
}