Commit | Line | Data |
---|---|---|
a3b510ab NR |
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 | } |