package com.googlecode.lanterna.graphics;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
import com.googlecode.lanterna.SGR;
import com.googlecode.lanterna.TerminalPosition;
import com.googlecode.lanterna.TerminalTextUtils;
import com.googlecode.lanterna.TextCharacter;
import com.googlecode.lanterna.TextColor;
import com.googlecode.lanterna.screen.TabBehaviour;
import com.googlecode.lanterna.screen.WrapBehaviour;
public class TextGraphicsWriter implements StyleSet<TextGraphicsWriter> {
private final TextGraphics backend;
private TerminalPosition cursorPosition;
private TextColor foregroundColor, backgroundColor;
private EnumSet<SGR> style = EnumSet.noneOf(SGR.class);
private WrapBehaviour wrapBehaviour = WrapBehaviour.WORD;
private boolean styleable = true;
public TextGraphicsWriter(TextGraphics backend) {
this.backend = backend;
setStyleFrom( backend );
cursorPosition = new TerminalPosition(0, 0);
}
public TextGraphicsWriter putString(String string) {
StringBuilder wordpart = new StringBuilder();
StyleSet.Set originalStyle = new StyleSet.Set(backend);
backend.setStyleFrom(this);
int wordlen = 0; // the whole column-length of the word.
for (int i = 0; i < string.length(); i++) {
char ch = string.charAt(i);
switch (ch) {
case '\n':
flush(wordpart,wordlen); wordlen = 0;
linefeed(-1); // -1 means explicit.
break;
case '\t':
flush(wordpart,wordlen); wordlen = 0;
if (backend.getTabBehaviour() != TabBehaviour.IGNORE) {
String repl = backend.getTabBehaviour()
.getTabReplacement(cursorPosition.getColumn());
for(int j = 0; j < repl.length(); j++) {
backend.setCharacter(cursorPosition.withRelativeColumn(j), repl.charAt(j));
}
cursorPosition = cursorPosition.withRelativeColumn(repl.length());
} else {
linefeed(2); putControlChar(ch);
}
break;
case '\033':
if (isStyleable()) {
stash(wordpart,wordlen);
String seq = TerminalTextUtils.getANSIControlSequenceAt(string, i);
TerminalTextUtils.updateModifiersFromCSICode(seq, this, originalStyle);
backend.setStyleFrom(this);
i += seq.length() - 1;
} else {
flush(wordpart,wordlen); wordlen = 0;
linefeed(2); putControlChar(ch);
}
break;
default:
if (Character.isISOControl(ch)) {
flush(wordpart,wordlen); wordlen = 0;
linefeed(1); putControlChar(ch);
} else if (Character.isWhitespace(ch)) {
flush(wordpart,wordlen); wordlen = 0;
backend.setCharacter(cursorPosition, ch);
cursorPosition = cursorPosition.withRelativeColumn(1);
} else if (TerminalTextUtils.isCharCJK(ch)) {
flush(wordpart, wordlen); wordlen = 0;
linefeed(2);
backend.setCharacter(cursorPosition, ch);
cursorPosition = cursorPosition.withRelativeColumn(2);
} else {
if (wrapBehaviour.keepWords()) {
// TODO: if at end of line despite starting at col 0, then split word.
wordpart.append(ch); wordlen++;
} else {
linefeed(1);
backend.setCharacter(cursorPosition, ch);
cursorPosition = cursorPosition.withRelativeColumn(1);
}
}
}
linefeed(wordlen);
}
flush(wordpart,wordlen);
backend.setStyleFrom(originalStyle);
return this;
}
private void linefeed(int lenToFit) {
int curCol = cursorPosition.getColumn();
int spaceLeft = backend.getSize().getColumns() - curCol;
if (wrapBehaviour.allowLineFeed()) {
boolean wantWrap = curCol > 0 && lenToFit > spaceLeft;
if (lenToFit < 0 || ( wantWrap && wrapBehaviour.autoWrap() ) ) {
// TODO: clear to end of current line?
cursorPosition = cursorPosition.withColumn(0).withRelativeRow(1);
}
} else {
if (lenToFit < 0) { // encode explicit line feed
putControlChar('\n');
}
}
}
public void putControlChar(char ch) {
char subst;
switch (ch) {
case '\033': subst = '['; break;
case '\034': subst = '\\'; break;
case '\035': subst = ']'; break;
case '\036': subst = '^'; break;
case '\037': subst = '_'; break;
case '\177': subst = '?'; break;
default:
if (ch <= 26) {
subst = (char)(ch + '@');
} else { // normal character - or 0x80-0x9F
// just write it out, anyway:
backend.setCharacter(cursorPosition, ch);
cursorPosition = cursorPosition.withRelativeColumn(1);
return;
}
}
EnumSet<SGR> style = getActiveModifiers();
if (style.contains(SGR.REVERSE)) {
style.remove(SGR.REVERSE);
} else {
style.add(SGR.REVERSE);
}
TextCharacter tc = new TextCharacter('^',
getForegroundColor(), getBackgroundColor(), style);
backend.setCharacter(cursorPosition, tc);
cursorPosition = cursorPosition.withRelativeColumn(1);
tc = tc.withCharacter(subst);
backend.setCharacter(cursorPosition, tc);
cursorPosition = cursorPosition.withRelativeColumn(1);
}
// A word (a sequence of characters that is kept together when word-wrapping)
// may consist of differently styled parts. This class describes one such
// part.
private static class WordPart extends StyleSet.Set {
String word; int wordlen;
WordPart(String word, int wordlen, StyleSet<?> style) {
this.word = word; this.wordlen = wordlen;
setStyleFrom(style);
}
}
private List<WordPart> chunk_queue = new ArrayList<WordPart>();
private void stash(StringBuilder word, int wordlen) {
if (word.length() > 0) {
WordPart chunk = new WordPart(word.toString(),wordlen, this);
chunk_queue.add(chunk);
// for convenience the StringBuilder is reset:
word.setLength(0);
}
}
private void flush(StringBuilder word, int wordlen) {
stash(word, wordlen);
if (chunk_queue.isEmpty()) {
return;
}
int row = cursorPosition.getRow();
int col = cursorPosition.getColumn();
int offset = 0;
for (WordPart chunk : chunk_queue) {
backend.setStyleFrom(chunk);
backend.putString(col+offset,row, chunk.word);
offset = chunk.wordlen;
}
chunk_queue.clear(); // they're done.
// set cursor right behind the word:
cursorPosition = cursorPosition.withColumn(col+offset);
backend.setStyleFrom(this);
}
/**
* @return the cursor position
*/
public TerminalPosition getCursorPosition() {
return cursorPosition;
}
/**
* @param cursorPosition the cursor position to set
*/
public void setCursorPosition(TerminalPosition cursorPosition) {
this.cursorPosition = cursorPosition;
}
/**
* @return the foreground color
*/
public TextColor getForegroundColor() {
return foregroundColor;
}
/**
* @param foreground the foreground color to set
*/
public TextGraphicsWriter setForegroundColor(TextColor foreground) {
this.foregroundColor = foreground;
return this;
}
/**
* @return the background color
*/
public TextColor getBackgroundColor() {
return backgroundColor;
}
/**
* @param background the background color to set
*/
public TextGraphicsWriter setBackgroundColor(TextColor background) {
this.backgroundColor = background;
return this;
}
@Override
public TextGraphicsWriter enableModifiers(SGR... modifiers) {
style.addAll(Arrays.asList(modifiers));
return this;
}
@Override
public TextGraphicsWriter disableModifiers(SGR... modifiers) {
style.removeAll(Arrays.asList(modifiers));
return this;
}
@Override
public TextGraphicsWriter setModifiers(EnumSet<SGR> modifiers) {
style.clear(); style.addAll(modifiers);
return this;
}
@Override
public TextGraphicsWriter clearModifiers() {
style.clear();
return this;
}
@Override
public EnumSet<SGR> getActiveModifiers() {
return EnumSet.copyOf(style);
}
@Override
public TextGraphicsWriter setStyleFrom(StyleSet<?> source) {
setBackgroundColor(source.getBackgroundColor());
setForegroundColor(source.getForegroundColor());
setModifiers(source.getActiveModifiers());
return this;
}
/**
* @return the wrapBehaviour
*/
public WrapBehaviour getWrapBehaviour() {
return wrapBehaviour;
}
/**
* @param wrapBehaviour the wrapBehaviour to set
*/
public void setWrapBehaviour(WrapBehaviour wrapBehaviour) {
this.wrapBehaviour = wrapBehaviour;
}
/**
* @return whether styles in strings are handled.
*/
public boolean isStyleable() {
return styleable;
}
/**
* @param styleable whether styles in strings should be handled.
*/
public void setStyleable(boolean styleable) {
this.styleable = styleable;
}
}