Fix UTF8 bug, create first executable JAR file
[jvcard.git] / src / com / googlecode / lanterna / screen / TerminalScreen.java
... / ...
CommitLineData
1/*
2 * This file is part of lanterna (http://code.google.com/p/lanterna/).
3 *
4 * lanterna is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU Lesser General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 *
17 * Copyright (C) 2010-2015 Martin
18 */
19package com.googlecode.lanterna.screen;
20
21import com.googlecode.lanterna.*;
22import com.googlecode.lanterna.graphics.Scrollable;
23import com.googlecode.lanterna.input.KeyStroke;
24import com.googlecode.lanterna.input.KeyType;
25import com.googlecode.lanterna.terminal.ResizeListener;
26import com.googlecode.lanterna.terminal.Terminal;
27
28import java.io.IOException;
29import java.util.Comparator;
30import java.util.EnumSet;
31import java.util.Map;
32import java.util.TreeMap;
33
34/**
35 * This is the default concrete implementation of the Screen interface, a buffered layer sitting on top of a Terminal.
36 * If you want to get started with the Screen layer, this is probably the class you want to use. Remember to start the
37 * screen before you can use it and stop it when you are done with it. This will place the terminal in private mode
38 * during the screen operations and leave private mode afterwards.
39 * @author martin
40 */
41public class TerminalScreen extends AbstractScreen {
42 private final Terminal terminal;
43 private boolean isStarted;
44 private boolean fullRedrawHint;
45 private ScrollHint scrollHint;
46
47 /**
48 * Creates a new Screen on top of a supplied terminal, will query the terminal for its size. The screen is initially
49 * blank. The default character used for unused space (the newly initialized state of the screen and new areas after
50 * expanding the terminal size) will be a blank space in 'default' ANSI front- and background color.
51 * <p>
52 * Before you can display the content of this buffered screen to the real underlying terminal, you must call the
53 * {@code startScreen()} method. This will ask the terminal to enter private mode (which is required for Screens to
54 * work properly). Similarly, when you are done, you should call {@code stopScreen()} which will exit private mode.
55 *
56 * @param terminal Terminal object to create the DefaultScreen on top of
57 * @throws java.io.IOException If there was an underlying I/O error when querying the size of the terminal
58 */
59 public TerminalScreen(Terminal terminal) throws IOException {
60 this(terminal, DEFAULT_CHARACTER);
61 }
62
63 /**
64 * Creates a new Screen on top of a supplied terminal, will query the terminal for its size. The screen is initially
65 * blank. The default character used for unused space (the newly initialized state of the screen and new areas after
66 * expanding the terminal size) will be a blank space in 'default' ANSI front- and background color.
67 * <p>
68 * Before you can display the content of this buffered screen to the real underlying terminal, you must call the
69 * {@code startScreen()} method. This will ask the terminal to enter private mode (which is required for Screens to
70 * work properly). Similarly, when you are done, you should call {@code stopScreen()} which will exit private mode.
71 *
72 * @param terminal Terminal object to create the DefaultScreen on top of.
73 * @param defaultCharacter What character to use for the initial state of the screen and expanded areas
74 * @throws java.io.IOException If there was an underlying I/O error when querying the size of the terminal
75 */
76 public TerminalScreen(Terminal terminal, TextCharacter defaultCharacter) throws IOException {
77 super(terminal.getTerminalSize(), defaultCharacter);
78 this.terminal = terminal;
79 this.terminal.addResizeListener(new TerminalResizeListener());
80 this.isStarted = false;
81 this.fullRedrawHint = true;
82 }
83
84 @Override
85 public synchronized void startScreen() throws IOException {
86 if(isStarted) {
87 return;
88 }
89
90 isStarted = true;
91 getTerminal().enterPrivateMode();
92 getTerminal().getTerminalSize();
93 getTerminal().clearScreen();
94 this.fullRedrawHint = true;
95 TerminalPosition cursorPosition = getCursorPosition();
96 if(cursorPosition != null) {
97 getTerminal().setCursorVisible(true);
98 getTerminal().setCursorPosition(cursorPosition.getColumn(), cursorPosition.getRow());
99 } else {
100 getTerminal().setCursorVisible(false);
101 }
102 }
103
104 @Override
105 public void stopScreen() throws IOException {
106 stopScreen(true);
107 }
108
109 public synchronized void stopScreen(boolean flushInput) throws IOException {
110 if(!isStarted) {
111 return;
112 }
113
114 if (flushInput) {
115 //Drain the input queue
116 KeyStroke keyStroke;
117 do {
118 keyStroke = pollInput();
119 }
120 while(keyStroke != null && keyStroke.getKeyType() != KeyType.EOF);
121 }
122
123 getTerminal().exitPrivateMode();
124 isStarted = false;
125 }
126
127 @Override
128 public synchronized void refresh(RefreshType refreshType) throws IOException {
129 if(!isStarted) {
130 return;
131 }
132 if((refreshType == RefreshType.AUTOMATIC && fullRedrawHint) || refreshType == RefreshType.COMPLETE) {
133 refreshFull();
134 fullRedrawHint = false;
135 }
136 else if(refreshType == RefreshType.AUTOMATIC &&
137 (scrollHint == null || scrollHint == ScrollHint.INVALID)) {
138 double threshold = getTerminalSize().getRows() * getTerminalSize().getColumns() * 0.75;
139 if(getBackBuffer().isVeryDifferent(getFrontBuffer(), (int) threshold)) {
140 refreshFull();
141 }
142 else {
143 refreshByDelta();
144 }
145 }
146 else {
147 refreshByDelta();
148 }
149 getBackBuffer().copyTo(getFrontBuffer());
150 TerminalPosition cursorPosition = getCursorPosition();
151 if(cursorPosition != null) {
152 getTerminal().setCursorVisible(true);
153 //If we are trying to move the cursor to the padding of a CJK character, put it on the actual character instead
154 if(cursorPosition.getColumn() > 0 && TerminalTextUtils.isCharCJK(getFrontBuffer().getCharacterAt(cursorPosition.withRelativeColumn(-1)).getCharacter())) {
155 getTerminal().setCursorPosition(cursorPosition.getColumn() - 1, cursorPosition.getRow());
156 }
157 else {
158 getTerminal().setCursorPosition(cursorPosition.getColumn(), cursorPosition.getRow());
159 }
160 } else {
161 getTerminal().setCursorVisible(false);
162 }
163 getTerminal().flush();
164 }
165
166 private void useScrollHint() throws IOException {
167 if (scrollHint == null) { return; }
168
169 try {
170 if (scrollHint == ScrollHint.INVALID) { return; }
171 Terminal term = getTerminal();
172 if (term instanceof Scrollable) {
173 // just try and see if it cares:
174 scrollHint.applyTo( (Scrollable)term );
175 // if that didn't throw, then update front buffer:
176 scrollHint.applyTo( getFrontBuffer() );
177 }
178 }
179 catch (UnsupportedOperationException uoe) { /* ignore */ }
180 finally { scrollHint = null; }
181 }
182
183 private void refreshByDelta() throws IOException {
184 Map<TerminalPosition, TextCharacter> updateMap = new TreeMap<TerminalPosition, TextCharacter>(new ScreenPointComparator());
185 TerminalSize terminalSize = getTerminalSize();
186
187 useScrollHint();
188
189 for(int y = 0; y < terminalSize.getRows(); y++) {
190 for(int x = 0; x < terminalSize.getColumns(); x++) {
191 TextCharacter backBufferCharacter = getBackBuffer().getCharacterAt(x, y);
192 if(!backBufferCharacter.equals(getFrontBuffer().getCharacterAt(x, y))) {
193 updateMap.put(new TerminalPosition(x, y), backBufferCharacter);
194 }
195 if(TerminalTextUtils.isCharCJK(backBufferCharacter.getCharacter())) {
196 x++; //Skip the trailing padding
197 }
198 }
199 }
200
201 if(updateMap.isEmpty()) {
202 return;
203 }
204 TerminalPosition currentPosition = updateMap.keySet().iterator().next();
205 getTerminal().setCursorPosition(currentPosition.getColumn(), currentPosition.getRow());
206
207 TextCharacter firstScreenCharacterToUpdate = updateMap.values().iterator().next();
208 EnumSet<SGR> currentSGR = firstScreenCharacterToUpdate.getModifiers();
209 getTerminal().resetColorAndSGR();
210 for(SGR sgr: currentSGR) {
211 getTerminal().enableSGR(sgr);
212 }
213 TextColor currentForegroundColor = firstScreenCharacterToUpdate.getForegroundColor();
214 TextColor currentBackgroundColor = firstScreenCharacterToUpdate.getBackgroundColor();
215 getTerminal().setForegroundColor(currentForegroundColor);
216 getTerminal().setBackgroundColor(currentBackgroundColor);
217 for(TerminalPosition position: updateMap.keySet()) {
218 if(!position.equals(currentPosition)) {
219 getTerminal().setCursorPosition(position.getColumn(), position.getRow());
220 currentPosition = position;
221 }
222 TextCharacter newCharacter = updateMap.get(position);
223 if(!currentForegroundColor.equals(newCharacter.getForegroundColor())) {
224 getTerminal().setForegroundColor(newCharacter.getForegroundColor());
225 currentForegroundColor = newCharacter.getForegroundColor();
226 }
227 if(!currentBackgroundColor.equals(newCharacter.getBackgroundColor())) {
228 getTerminal().setBackgroundColor(newCharacter.getBackgroundColor());
229 currentBackgroundColor = newCharacter.getBackgroundColor();
230 }
231 for(SGR sgr: SGR.values()) {
232 if(currentSGR.contains(sgr) && !newCharacter.getModifiers().contains(sgr)) {
233 getTerminal().disableSGR(sgr);
234 currentSGR.remove(sgr);
235 }
236 else if(!currentSGR.contains(sgr) && newCharacter.getModifiers().contains(sgr)) {
237 getTerminal().enableSGR(sgr);
238 currentSGR.add(sgr);
239 }
240 }
241 getTerminal().putCharacter(newCharacter.getCharacter());
242 if(TerminalTextUtils.isCharCJK(newCharacter.getCharacter())) {
243 //CJK characters advances two columns
244 currentPosition = currentPosition.withRelativeColumn(2);
245 }
246 else {
247 //Normal characters advances one column
248 currentPosition = currentPosition.withRelativeColumn(1);
249 }
250 }
251 }
252
253 private void refreshFull() throws IOException {
254 getTerminal().setForegroundColor(TextColor.ANSI.DEFAULT);
255 getTerminal().setBackgroundColor(TextColor.ANSI.DEFAULT);
256 getTerminal().clearScreen();
257 getTerminal().resetColorAndSGR();
258 scrollHint = null; // discard any scroll hint for full refresh
259
260 EnumSet<SGR> currentSGR = EnumSet.noneOf(SGR.class);
261 TextColor currentForegroundColor = TextColor.ANSI.DEFAULT;
262 TextColor currentBackgroundColor = TextColor.ANSI.DEFAULT;
263 for(int y = 0; y < getTerminalSize().getRows(); y++) {
264 getTerminal().setCursorPosition(0, y);
265 int currentColumn = 0;
266 for(int x = 0; x < getTerminalSize().getColumns(); x++) {
267 TextCharacter newCharacter = getBackBuffer().getCharacterAt(x, y);
268 if(newCharacter.equals(DEFAULT_CHARACTER)) {
269 continue;
270 }
271
272 if(!currentForegroundColor.equals(newCharacter.getForegroundColor())) {
273 getTerminal().setForegroundColor(newCharacter.getForegroundColor());
274 currentForegroundColor = newCharacter.getForegroundColor();
275 }
276 if(!currentBackgroundColor.equals(newCharacter.getBackgroundColor())) {
277 getTerminal().setBackgroundColor(newCharacter.getBackgroundColor());
278 currentBackgroundColor = newCharacter.getBackgroundColor();
279 }
280 for(SGR sgr: SGR.values()) {
281 if(currentSGR.contains(sgr) && !newCharacter.getModifiers().contains(sgr)) {
282 getTerminal().disableSGR(sgr);
283 currentSGR.remove(sgr);
284 }
285 else if(!currentSGR.contains(sgr) && newCharacter.getModifiers().contains(sgr)) {
286 getTerminal().enableSGR(sgr);
287 currentSGR.add(sgr);
288 }
289 }
290 if(currentColumn != x) {
291 getTerminal().setCursorPosition(x, y);
292 currentColumn = x;
293 }
294 getTerminal().putCharacter(newCharacter.getCharacter());
295 if(TerminalTextUtils.isCharCJK(newCharacter.getCharacter())) {
296 //CJK characters take up two columns
297 currentColumn += 2;
298 x++;
299 }
300 else {
301 //Normal characters take up one column
302 currentColumn += 1;
303 }
304 }
305 }
306 }
307
308 /**
309 * Returns the underlying {@code Terminal} interface that this Screen is using.
310 * <p>
311 * <b>Be aware:</b> directly modifying the underlying terminal will most likely result in unexpected behaviour if
312 * you then go on and try to interact with the Screen. The Screen's back-buffer/front-buffer will not know about
313 * the operations you are going on the Terminal and won't be able to properly generate a refresh unless you enforce
314 * a {@code Screen.RefreshType.COMPLETE}, at which the entire terminal area will be repainted according to the
315 * back-buffer of the {@code Screen}.
316 * @return Underlying terminal used by the screen
317 */
318 @SuppressWarnings("WeakerAccess")
319 public Terminal getTerminal() {
320 return terminal;
321 }
322
323 @Override
324 public KeyStroke readInput() throws IOException {
325 return terminal.readInput();
326 }
327
328 @Override
329 public KeyStroke pollInput() throws IOException {
330 return terminal.pollInput();
331 }
332
333 @Override
334 public synchronized void clear() {
335 super.clear();
336 fullRedrawHint = true;
337 scrollHint = ScrollHint.INVALID;
338 }
339
340 @Override
341 public synchronized TerminalSize doResizeIfNecessary() {
342 TerminalSize newSize = super.doResizeIfNecessary();
343 if(newSize != null) {
344 fullRedrawHint = true;
345 }
346 return newSize;
347 }
348
349 /**
350 * Perform the scrolling and save scroll-range and distance in order
351 * to be able to optimize Terminal-update later.
352 */
353 @Override
354 public void scrollLines(int firstLine, int lastLine, int distance) {
355 // just ignore certain kinds of garbage:
356 if (distance == 0 || firstLine > lastLine) { return; }
357
358 super.scrollLines(firstLine, lastLine, distance);
359
360 // Save scroll hint for next refresh:
361 ScrollHint newHint = new ScrollHint(firstLine,lastLine,distance);
362 if (scrollHint == null) {
363 // no scroll hint yet: use the new one:
364 scrollHint = newHint;
365 } else if (scrollHint == ScrollHint.INVALID) {
366 // scroll ranges already inconsistent since latest refresh!
367 // leave at INVALID
368 } else if (scrollHint.matches(newHint)) {
369 // same range: just accumulate distance:
370 scrollHint.distance += newHint.distance;
371 } else {
372 // different scroll range: no scroll-optimization for next refresh
373 this.scrollHint = ScrollHint.INVALID;
374 }
375 }
376
377 private class TerminalResizeListener implements ResizeListener {
378 @Override
379 public void onResized(Terminal terminal, TerminalSize newSize) {
380 addResizeRequest(newSize);
381 }
382 }
383
384 private static class ScreenPointComparator implements Comparator<TerminalPosition> {
385 @Override
386 public int compare(TerminalPosition o1, TerminalPosition o2) {
387 if(o1.getRow() == o2.getRow()) {
388 if(o1.getColumn() == o2.getColumn()) {
389 return 0;
390 } else {
391 return new Integer(o1.getColumn()).compareTo(o2.getColumn());
392 }
393 } else {
394 return new Integer(o1.getRow()).compareTo(o2.getRow());
395 }
396 }
397 }
398
399 private static class ScrollHint {
400 public static final ScrollHint INVALID = new ScrollHint(-1,-1,0);
401 public int firstLine, lastLine, distance;
402
403 public ScrollHint(int firstLine, int lastLine, int distance) {
404 this.firstLine = firstLine;
405 this.lastLine = lastLine;
406 this.distance = distance;
407 }
408
409 public boolean matches(ScrollHint other) {
410 return this.firstLine == other.firstLine
411 && this.lastLine == other.lastLine;
412 }
413
414 public void applyTo( Scrollable scr ) throws IOException {
415 scr.scrollLines(firstLine, lastLine, distance);
416 }
417 }
418
419}