Change build scripts
[jvcard.git] / src / com / googlecode / lanterna / screen / VirtualScreen.java
1 package com.googlecode.lanterna.screen;
2
3 import com.googlecode.lanterna.*;
4 import com.googlecode.lanterna.graphics.TextGraphics;
5 import com.googlecode.lanterna.input.KeyStroke;
6 import com.googlecode.lanterna.input.KeyType;
7
8 import java.io.IOException;
9
10 /**
11 * VirtualScreen wraps a normal screen and presents it as a screen that has a configurable minimum size; if the real
12 * screen is smaller than this size, the presented screen will add scrolling to get around it. To anyone using this
13 * class, it will appear and behave just as a normal screen. Scrolling is done by using CTRL + arrow keys.
14 * <p>
15 * The use case for this class is to allow you to set a minimum size that you can count on be honored, no matter how
16 * small the user makes the terminal. This should make programming GUIs easier.
17 * @author Martin
18 */
19 public class VirtualScreen extends AbstractScreen {
20 private final Screen realScreen;
21 private final FrameRenderer frameRenderer;
22 private TerminalSize minimumSize;
23 private TerminalPosition viewportTopLeft;
24 private TerminalSize viewportSize;
25
26 /**
27 * Creates a new VirtualScreen that wraps a supplied Screen. The screen passed in here should be the real screen
28 * that is created on top of the real {@code Terminal}, it will have the correct size and content for what's
29 * actually displayed to the user, but this class will present everything as one view with a fixed minimum size,
30 * no matter what size the real terminal has.
31 * <p>
32 * The initial minimum size will be the current size of the screen.
33 * @param screen Real screen that will be used when drawing the whole or partial virtual screen
34 */
35 public VirtualScreen(Screen screen) {
36 super(screen.getTerminalSize());
37 this.frameRenderer = new DefaultFrameRenderer();
38 this.realScreen = screen;
39 this.minimumSize = screen.getTerminalSize();
40 this.viewportTopLeft = TerminalPosition.TOP_LEFT_CORNER;
41 this.viewportSize = minimumSize;
42 }
43
44 /**
45 * Sets the minimum size we want the virtual screen to have. If the user resizes the real terminal to something
46 * smaller than this, the virtual screen will refuse to make it smaller and add scrollbars to the view.
47 * @param minimumSize Minimum size we want the screen to have
48 */
49 public void setMinimumSize(TerminalSize minimumSize) {
50 this.minimumSize = minimumSize;
51 TerminalSize virtualSize = minimumSize.max(realScreen.getTerminalSize());
52 if(!minimumSize.equals(virtualSize)) {
53 addResizeRequest(virtualSize);
54 super.doResizeIfNecessary();
55 }
56 calculateViewport(realScreen.getTerminalSize());
57 }
58
59 /**
60 * Returns the minimum size this virtual screen can have. If the real terminal is made smaller than this, the
61 * virtual screen will draw scrollbars and implement scrolling
62 * @return Minimum size configured for this virtual screen
63 */
64 public TerminalSize getMinimumSize() {
65 return minimumSize;
66 }
67
68 @Override
69 public void startScreen() throws IOException {
70 realScreen.startScreen();
71 }
72
73 @Override
74 public void stopScreen() throws IOException {
75 realScreen.stopScreen();
76 }
77
78 @Override
79 public TextCharacter getFrontCharacter(TerminalPosition position) {
80 return null;
81 }
82
83 @Override
84 public void setCursorPosition(TerminalPosition position) {
85 super.setCursorPosition(position);
86 if(position == null) {
87 realScreen.setCursorPosition(null);
88 return;
89 }
90 position = position.withRelativeColumn(-viewportTopLeft.getColumn()).withRelativeRow(-viewportTopLeft.getRow());
91 if(position.getColumn() >= 0 && position.getColumn() < viewportSize.getColumns() &&
92 position.getRow() >= 0 && position.getRow() < viewportSize.getRows()) {
93 realScreen.setCursorPosition(position);
94 }
95 else {
96 realScreen.setCursorPosition(null);
97 }
98 }
99
100 @Override
101 public synchronized TerminalSize doResizeIfNecessary() {
102 TerminalSize underlyingSize = realScreen.doResizeIfNecessary();
103 if(underlyingSize == null) {
104 return null;
105 }
106
107 TerminalSize newVirtualSize = calculateViewport(underlyingSize);
108 if(!getTerminalSize().equals(newVirtualSize)) {
109 addResizeRequest(newVirtualSize);
110 return super.doResizeIfNecessary();
111 }
112 return newVirtualSize;
113 }
114
115 private TerminalSize calculateViewport(TerminalSize realTerminalSize) {
116 TerminalSize newVirtualSize = minimumSize.max(realTerminalSize);
117 if(newVirtualSize.equals(realTerminalSize)) {
118 viewportSize = realTerminalSize;
119 viewportTopLeft = TerminalPosition.TOP_LEFT_CORNER;
120 }
121 else {
122 TerminalSize newViewportSize = frameRenderer.getViewportSize(realTerminalSize, newVirtualSize);
123 if(newViewportSize.getRows() > viewportSize.getRows()) {
124 viewportTopLeft = viewportTopLeft.withRow(Math.max(0, viewportTopLeft.getRow() - (newViewportSize.getRows() - viewportSize.getRows())));
125 }
126 if(newViewportSize.getColumns() > viewportSize.getColumns()) {
127 viewportTopLeft = viewportTopLeft.withColumn(Math.max(0, viewportTopLeft.getColumn() - (newViewportSize.getColumns() - viewportSize.getColumns())));
128 }
129 viewportSize = newViewportSize;
130 }
131 return newVirtualSize;
132 }
133
134 @Override
135 public void refresh(RefreshType refreshType) throws IOException {
136 setCursorPosition(getCursorPosition()); //Make sure the cursor is at the correct position
137 if(!viewportSize.equals(realScreen.getTerminalSize())) {
138 frameRenderer.drawFrame(
139 realScreen.newTextGraphics(),
140 realScreen.getTerminalSize(),
141 getTerminalSize(),
142 viewportTopLeft);
143 }
144
145 //Copy the rows
146 TerminalPosition viewportOffset = frameRenderer.getViewportOffset();
147 if(realScreen instanceof AbstractScreen) {
148 AbstractScreen asAbstractScreen = (AbstractScreen)realScreen;
149 getBackBuffer().copyTo(
150 asAbstractScreen.getBackBuffer(),
151 viewportTopLeft.getRow(),
152 viewportSize.getRows(),
153 viewportTopLeft.getColumn(),
154 viewportSize.getColumns(),
155 viewportOffset.getRow(),
156 viewportOffset.getColumn());
157 }
158 else {
159 for(int y = 0; y < viewportSize.getRows(); y++) {
160 for(int x = 0; x < viewportSize.getColumns(); x++) {
161 realScreen.setCharacter(
162 x + viewportOffset.getColumn(),
163 y + viewportOffset.getRow(),
164 getBackBuffer().getCharacterAt(
165 x + viewportTopLeft.getColumn(),
166 y + viewportTopLeft.getRow()));
167 }
168 }
169 }
170 realScreen.refresh(refreshType);
171 }
172
173 @Override
174 public KeyStroke pollInput() throws IOException {
175 return filter(realScreen.pollInput());
176 }
177
178 @Override
179 public KeyStroke readInput() throws IOException {
180 return filter(realScreen.readInput());
181 }
182
183 private KeyStroke filter(KeyStroke keyStroke) throws IOException {
184 if(keyStroke == null) {
185 return null;
186 }
187 else if(keyStroke.isAltDown() && keyStroke.getKeyType() == KeyType.ArrowLeft) {
188 if(viewportTopLeft.getColumn() > 0) {
189 viewportTopLeft = viewportTopLeft.withRelativeColumn(-1);
190 refresh();
191 return null;
192 }
193 }
194 else if(keyStroke.isAltDown() && keyStroke.getKeyType() == KeyType.ArrowRight) {
195 if(viewportTopLeft.getColumn() + viewportSize.getColumns() < getTerminalSize().getColumns()) {
196 viewportTopLeft = viewportTopLeft.withRelativeColumn(1);
197 refresh();
198 return null;
199 }
200 }
201 else if(keyStroke.isAltDown() && keyStroke.getKeyType() == KeyType.ArrowUp) {
202 if(viewportTopLeft.getRow() > 0) {
203 viewportTopLeft = viewportTopLeft.withRelativeRow(-1);
204 realScreen.scrollLines(0,viewportSize.getRows()-1,-1);
205 refresh();
206 return null;
207 }
208 }
209 else if(keyStroke.isAltDown() && keyStroke.getKeyType() == KeyType.ArrowDown) {
210 if(viewportTopLeft.getRow() + viewportSize.getRows() < getTerminalSize().getRows()) {
211 viewportTopLeft = viewportTopLeft.withRelativeRow(1);
212 realScreen.scrollLines(0,viewportSize.getRows()-1,1);
213 refresh();
214 return null;
215 }
216 }
217 return keyStroke;
218 }
219
220 @Override
221 public void scrollLines(int firstLine, int lastLine, int distance) {
222 // do base class stuff (scroll own back buffer)
223 super.scrollLines(firstLine, lastLine, distance);
224 // vertical range visible in realScreen:
225 int vpFirst = viewportTopLeft.getRow(),
226 vpRows = viewportSize.getRows();
227 // adapt to realScreen range:
228 firstLine = Math.max(0, firstLine - vpFirst);
229 lastLine = Math.min(vpRows - 1, lastLine - vpFirst);
230 // if resulting range non-empty: scroll that range in realScreen:
231 if (firstLine <= lastLine) {
232 realScreen.scrollLines(firstLine, lastLine, distance);
233 }
234 }
235
236 /**
237 * Interface for rendering the virtual screen's frame when the real terminal is too small for the virtual screen
238 */
239 public interface FrameRenderer {
240 /**
241 * Given the size of the real terminal and the current size of the virtual screen, how large should the viewport
242 * where the screen content is drawn be?
243 * @param realSize Size of the real terminal
244 * @param virtualSize Size of the virtual screen
245 * @return Size of the viewport, according to this FrameRenderer
246 */
247 TerminalSize getViewportSize(TerminalSize realSize, TerminalSize virtualSize);
248
249 /**
250 * Where in the virtual screen should the top-left position of the viewport be? To draw the viewport from the
251 * top-left position of the screen, return 0x0 (or TerminalPosition.TOP_LEFT_CORNER) here.
252 * @return Position of the top-left corner of the viewport inside the screen
253 */
254 TerminalPosition getViewportOffset();
255
256 /**
257 * Drawn the 'frame', meaning anything that is outside the viewport (title, scrollbar, etc)
258 * @param graphics Graphics to use to text drawing operations
259 * @param realSize Size of the real terminal
260 * @param virtualSize Size of the virtual screen
261 * @param virtualScrollPosition If the virtual screen is larger than the real terminal, this is the current
262 * scroll offset the VirtualScreen is using
263 */
264 void drawFrame(
265 TextGraphics graphics,
266 TerminalSize realSize,
267 TerminalSize virtualSize,
268 TerminalPosition virtualScrollPosition);
269 }
270
271 private static class DefaultFrameRenderer implements FrameRenderer {
272 @Override
273 public TerminalSize getViewportSize(TerminalSize realSize, TerminalSize virtualSize) {
274 if(realSize.getColumns() > 1 && realSize.getRows() > 2) {
275 return realSize.withRelativeColumns(-1).withRelativeRows(-2);
276 }
277 else {
278 return realSize;
279 }
280 }
281
282 @Override
283 public TerminalPosition getViewportOffset() {
284 return TerminalPosition.TOP_LEFT_CORNER;
285 }
286
287 @Override
288 public void drawFrame(
289 TextGraphics graphics,
290 TerminalSize realSize,
291 TerminalSize virtualSize,
292 TerminalPosition virtualScrollPosition) {
293
294 if(realSize.getColumns() == 1 || realSize.getRows() <= 2) {
295 return;
296 }
297 TerminalSize viewportSize = getViewportSize(realSize, virtualSize);
298
299 graphics.setForegroundColor(TextColor.ANSI.WHITE);
300 graphics.setBackgroundColor(TextColor.ANSI.BLACK);
301 graphics.fill(' ');
302 graphics.putString(0, graphics.getSize().getRows() - 1, "Terminal too small, use ALT+arrows to scroll");
303
304 int horizontalSize = (int)(((double)(viewportSize.getColumns()) / (double)virtualSize.getColumns()) * (viewportSize.getColumns()));
305 int scrollable = viewportSize.getColumns() - horizontalSize - 1;
306 int horizontalPosition = (int)((double)scrollable * ((double)virtualScrollPosition.getColumn() / (double)(virtualSize.getColumns() - viewportSize.getColumns())));
307 graphics.drawLine(
308 new TerminalPosition(horizontalPosition, graphics.getSize().getRows() - 2),
309 new TerminalPosition(horizontalPosition + horizontalSize, graphics.getSize().getRows() - 2),
310 Symbols.BLOCK_MIDDLE);
311
312 int verticalSize = (int)(((double)(viewportSize.getRows()) / (double)virtualSize.getRows()) * (viewportSize.getRows()));
313 scrollable = viewportSize.getRows() - verticalSize - 1;
314 int verticalPosition = (int)((double)scrollable * ((double)virtualScrollPosition.getRow() / (double)(virtualSize.getRows() - viewportSize.getRows())));
315 graphics.drawLine(
316 new TerminalPosition(graphics.getSize().getColumns() - 1, verticalPosition),
317 new TerminalPosition(graphics.getSize().getColumns() - 1, verticalPosition + verticalSize),
318 Symbols.BLOCK_MIDDLE);
319 }
320 }
321 }